Microservices vs monolith: an honest take for smaller teams
For most teams under about 50 engineers, the microservices vs monolith question has a boring answer: build a well-structured monolith. Microservices solve organizational scaling problems – many teams needing to deploy independently – and they charge for it in network calls, distributed data and operational overhead. Until you have those problems, you pay the cost without the benefit.
That’s not a dogmatic position. There are good reasons to split a service out, and I’ll cover them. But the expensive version of this mistake – a small team running more services than it has engineers – is far more common than the other one.
What each architecture actually means
A monolithic architecture deploys the application as a single unit: one codebase, one build, usually one primary database. Rails, Django and Laravel apps are monoliths by default. Monolith doesn’t mean messy; a monolith can have strong internal boundaries, which is the idea behind a modular monolith.
Microservices split the application into independently deployable services, each owning its own data and communicating over the network – HTTP, gRPC or messages. The defining property isn’t size. It’s independent deployability and data ownership. Several services sharing one database and deployed together are a distributed monolith, which has the costs of both and the benefits of neither.
The real trade-offs
Here’s how the two compare on the dimensions that matter day to day:
- Local development: a monolith runs with one command. Microservices need a dozen containers, seeded data in each, and usually a shared staging environment that’s always broken.
- Transactions: in a monolith, “charge the card and create the subscription” is one database transaction. Across services it’s a saga with compensating actions, idempotent consumers and failure states you have to design for.
- Refactoring: moving a responsibility between modules is an IDE operation. Moving it between services is a versioned API change, a data migration and a coordinated deploy.
- Debugging: a stack trace versus distributed tracing across services, which you must build and maintain before you need it.
- Deploy independence: microservices win here. Team A ships without waiting for team B’s tests or release train.
- Scaling: microservices let you scale hot components separately. In practice, a monolith scales horizontally behind a load balancer for a very long time, and the bottleneck is almost always the database, which splitting the code doesn’t fix.
- Fault isolation: a memory leak in one service doesn’t take down the others – though synchronous call chains often reintroduce the coupling you were trying to escape.
- Technology choice: services can use different languages. This is more often a liability than a benefit for a small team.
Why I say start with a monolith
Martin Fowler’s “MonolithFirst” argument holds up: almost every successful microservices system he’d seen started as a monolith that got too big, and the systems built as microservices from scratch often ended up in trouble. The reason is boundaries. Good service boundaries follow the domain, and you don’t understand the domain until you’ve built in it for a while. Get them wrong in a monolith and you move some code. Get them wrong across services and you have chatty network calls, shared databases and coordinated deploys – a distributed monolith.
Plenty of large products run on monoliths. Shopify’s core is famously a large Rails monolith, organized into components rather than split into services. In my experience, from Yola’s growth from 10K to 11M users to much smaller SaaS products, the constraint that bites first is the data layer and the team’s ability to ship, not the deployment unit.
Conway’s law decides more than technology does
Mel Conway observed that systems end up mirroring the communication structure of the organizations that build them. That’s the real driver of microservices. When you have eight teams, each owning a business area, a single deploy pipeline becomes a coordination bottleneck, and splitting along team lines lets each team move at its own pace.
With one or two teams, there is no coordination bottleneck to remove. Splitting into services just creates handoffs inside the same team. A rough heuristic: you need at least one team per service that can own it end to end, including on-call. If your services outnumber your teams by three to one, you’ve probably split too early.
Signals that it’s time to split something out
Extracting a service is worth it when at least one of these is true, and you can name the specific component:
- Different scaling profile: a PDF renderer, image pipeline or ML inference workload that needs very different resources from the web tier.
- Different reliability requirement: a payments or webhook-ingest component that must keep accepting requests even when the main app is deploying or degraded.
- Different release cadence or ownership: a team that’s consistently blocked by others’ deploys, and owns a cleanly bounded area.
- Different security or compliance boundary: card data handling, or tenant data that must be isolated for enterprise contracts.
- A genuinely different runtime: a Python data service alongside a Rails app, for example.
Notice these are reasons to extract one service, not to adopt microservices as an architecture. Most healthy SaaS products settle at a core monolith plus a handful of well-justified services.
Monolith to microservices migration: how to do it safely
- Modularize first. Draw boundaries inside the monolith and enforce them. If you can’t keep a module from reaching into another module’s tables in-process, you won’t manage it across a network.
- Give the module its own data. Separate tables, no cross-module joins, access only through the module’s interface.
- Replace in-process calls with events where you can. Asynchronous integration through an outbox tolerates the other side being down; synchronous calls don’t. See event-driven architecture.
- Build the platform pieces: CI per service, centralized logging, distributed tracing, service-to-service auth, and a deploy process that doesn’t need a human.
- Extract with the strangler approach. Route calls to the new service behind a flag, compare, cut over, delete the old code. The strangler fig pattern guide covers the routing and data ownership details.
Going the other way
Merging services back into a monolith is a legitimate move, and more teams are doing it. If your services always deploy together, share a database, or require changes in three repos for one feature, consolidating them will usually make the team faster and cut infrastructure cost. It’s not an admission of failure; it’s adjusting the architecture to the team you actually have.
If you’re weighing either direction and want a second opinion grounded in your codebase rather than conference talks, that’s the kind of call I help with as a software architecture consultant.
Questions
Is a monolith better than microservices?
For most teams under roughly 50 engineers, yes. A well-structured monolith is simpler to develop, test, debug and deploy, and keeps transactions simple. Microservices pay off when many teams need to deploy independently or components have very different scaling or reliability needs.
When should you move from monolith to microservices?
When a specific component has a different scaling profile, reliability requirement, security boundary or owning team, and the monolith is actively slowing that team down. Move one component at a time, modularize it inside the monolith first, and give it its own data before extracting it.
What is a distributed monolith?
A system split into services that still share a database, must be deployed together, or make long chains of synchronous calls to each other. It has the operational cost of microservices without independent deployability, and it’s the most common outcome of splitting a monolith too early.
Do microservices scale better than a monolith?
They let you scale individual components separately, which helps when one workload is very different from the rest. But a monolith scales horizontally behind a load balancer for a long time, and the usual bottleneck is the database, which splitting application code doesn’t fix on its own.
Arguing about splitting the monolith?
I’ll look at your codebase, team shape and roadmap and give you a straight answer – then ship the first boundary or extraction with your team in a two-week sprint.
GMT+2 · Full EU overlap · US East mornings