Should you move to microservices? Monolith, modular monolith and the real question
For most teams the right answer is one application with well-drawn internal boundaries. Microservices are not a fix for code quality, they are a fix for deployment independence: they pay off when you have several teams working in parallel and each one has to ship to production without waiting for the others. In a single team of eight, the same decision converts the complexity of one application into the complexity of fifteen. The code stays exactly as coupled as it was, the calls between the parts just travel over a network now. What follows are the criteria worth using, the middle path of the modular monolith, and the measured results from teams that made this move and then reversed it.
What a microservice is, and what it is not
There is only one distinguishing item in the definition: a service is a microservice if it can be released to production independently of the others. Being small, living in its own repository, shipping as a container image or speaking REST means nothing on its own. The test is single: can you change service A and put it live the same day without touching B and C, and without waiting for their teams to sign off? If the answer is no, you do not have a microservice architecture. You have modules that are called over a network.
The dividing line is usually the database. Two services writing to the same tables are not independent: a schema change in one breaks the other, so they end up released together. A service that does not own its own data cannot be deployed on its own.
Distributed systems are not free
Martin Fowler calls the cost of this a microservice premium and draws the obvious conclusion from it: the premium is heavy enough that microservices only earn their place in sufficiently complex systems. Inside the premium sit network latency, partial failure, distributed tracing, service discovery, version compatibility and data consistency. His observation in MonolithFirst is sharper still: nearly every successful microservice story he has heard started with a monolith that grew too big and got broken up, while nearly every system built as microservices from scratch ended up in serious trouble.
He gives two reasons. First, on a new application you do not yet know which features will turn out to matter. Second, getting service boundaries right is genuinely hard, and moving functionality between services is far more expensive than moving it inside a monolith. Draw a boundary in the wrong place inside one codebase and you fix it with a refactor. Draw it in the wrong place between services and you have to coordinate two teams, two deployment pipelines and a data migration at once.
Then there is the line nobody puts in the estimate. Every service brings its own pipeline, its own dashboards, its own set of credentials and its own patching schedule, and that work grows in step with the number of services.
You need to clear a bar before you use this architecture
Fowler names three capabilities that have to be in place first: being able to stand up a new server in a matter of hours, having basic monitoring that covers both technical and business metrics, and running a deployment pipeline that finishes in no more than a couple of hours. Without those, do not attempt the move. The useful part is that all three are worth having for a monolith too, so you can start on them without deciding anything about architecture yet. Our posts on CI/CD for small teams and observability, SLOs and error budgets go through both.
Diagnose the complaint before you act on it
Two completely different problems arrive phrased the same way: "we need to split this up." They need separating.
Coupling complaints: a small change breaks unrelated things, the test suite takes far too long, a new developer spends three months moving through the code nervously. These are modularity problems. Splitting the system into services does not solve them, it spreads them across a network.
Independence complaints: two teams queue for the same release window, one module needs many times the resources of the rest, a single workload needs a different runtime or language, one component has an availability target markedly different from everything around it. Here, pulling a service out can genuinely be the answer.
The one measurable question: over the last three months, how many releases needed another team's approval or a simultaneous deployment? If that number is near zero, your architecture is not what is slowing you down.
Modular monolith: draw the boundaries, keep the process
Shopify's account is the best documented version of the middle path. By 2016 the single application had become a bottleneck, where a change that looked harmless could set off a cascade of failures in unrelated tests. They evaluated microservices, decided against them, and listed why: separate test and deployment pipelines to maintain, infrastructure overhead per service, network latency between services, difficulty reaching data on the other side of a boundary, and refactors that would require coordinated deployments across dependent services.
What they did instead was put strict boundaries inside the one application. In early 2017 they reorganised roughly 6,000 Ruby classes away from software concepts (models, controllers) and towards business domains (orders, shipping, inventory, billing), in a single large automated refactor. Then they built an internal tool called Wedge to make boundary violations visible, and later Packwerk to block them during development. Packwerk enforces two kinds of violation: dependency violations, where code references a private constant from a package it has not declared a dependency on, and privacy violations, where an external constant reaches into a package's private internals.
The practical value is this: you get the benefit of modularity, meaning legible boundaries, clear ownership and safe change, without paying the distributed systems bill. Once the boundaries are actually enforced in the code, extracting one module into a service becomes close to mechanical work. The reverse order does not work. Split a system you never drew boundaries in and what you get is a distributed monolith.
The team that came back from 140 services
Segment published the other side of this decision in 2018. They had built a separate service and a separate queue for every data destination; the count passed 140, each in its own repository. Updating the shared libraries meant testing and deploying more than 140 services, so engineers stayed on whichever version they had rather than carry that load, and dependency versions drifted apart across the codebases. Operational overhead grew linearly with each new destination, and the on-call engineer was being paged at night over load spikes on low-traffic destinations.
After consolidating back into a single service, the numbers they reported: a change to a shared library needs one deployment instead of 140-plus, and one engineer can ship it in minutes; a test suite that took a couple of minutes per destination now runs in milliseconds for all of them together; and where the microservice era produced 32 improvements to the shared libraries in a year, the year after the move produced 46. Their own caveat matters as much as the result. In some parts of their infrastructure microservices worked well. The decision was made about that workload, not about architecture in general.
What the Prime Video case proves, and what it does not
A headline went around in 2023: Amazon went back from microservices to a monolith and cut costs by 90%. The real story is narrower and more useful. The team monitoring audio and video quality at Prime Video had built a distributed solution with components orchestrated by AWS Step Functions. Two bottlenecks appeared: the number of state transitions ran into account limits, and holding video frames temporarily in S3 was expensive because the call volume was so high. Merging the components into a single process and passing the data in memory removed both bottlenecks and cut cost by 90%, and to scale they ran multiple copies of that process rather than one large instance.
This was not a change of company policy. It was one internal workload being moved to the architecture that suited it: for a data-heavy pipeline whose steps are tightly bound to each other, splitting the stages across a network cost more than it returned. The transferable lesson is that architecture decisions belong at the workload level, not the company level.
The worst outcome is a distributed monolith
The characteristic way this migration fails is neither microservices nor a monolith. It is the worst halves of both. The symptoms are recognisable: services are separate but all of them have to be released at the same time, several services write to the same database and one schema change breaks the chain, and every piece of work needs coordination between teams. Sam Newman offers a blunt indicator: if someone in your organisation now has a full-time job coordinating releases, this is what you have.
The cause is usually the same. The system gets split along existing code boundaries rather than business domains, meaning whatever was there is lifted as-is and scattered into separate processes. That structure moves slower than the monolith did, because in-process calls have been replaced by network calls while the need for coordination has not gone down at all.
The target is independent deployability, not microservices
DORA's research does not name microservices as the goal. It names loosely coupled architecture, and it points out that this is achievable with any technology, mainframes included. Their criteria work well as questions. Can you make large-scale design changes without permission from another team? Can you finish work without fine-grained coordination across teams? Can you deploy your product independently of the services it depends on? Can you run your tests whenever you want without needing a shared integrated environment? Can you release during business hours with negligible downtime? A well-built monolith can answer yes to all five. The moment it does, the microservice debate drops off the agenda by itself.
There is also evidence from the other end of the scale that this architecture does not organise itself. In 2020 Uber published a scheme grouping 2,200 microservices into 70 domains; product teams call a gateway for the domain instead of calling dozens of underlying services one by one, and because that cut the number of touchpoints needed to launch a feature, onboarding time dropped by 25% to 50%. In other words, past a certain service count you have to add a layer to win back the structure you lost.
Every new service is a new surface
Security work scales with service count: authentication between services, separate credentials and secrets to manage in each one, more base images to patch, more dependency trees to review, more endpoints talking on the internal network. Five services can be handled by hand; thirty cannot. If you do decide to split, put service identity and secrets management somewhere central from day one, because retrofitting it means doing the work once per service.
A first step that fits this week
Take three measurements before deciding anything. One: go through the release records for the last 90 days and count how many needed another team's approval or a simultaneous deployment. Two: profile resource use per module and ask whether any part genuinely needs to scale differently from the rest, and which one. Three: draw a data ownership map showing how many modules write to each table.
If the first number is low, your architecture is not the constraint and the problem sits elsewhere; in that case measuring and prioritising technical debt will be more use to you. If the third number is high, you are not ready to split and data ownership has to be separated first. A concrete starting point: write down module boundaries by business domain inside the monolith and enforce them with static analysis. When you do pull the first service out, pick a capability that sits at the edge, owns its own data and gets called often. Plan the sequence and the rollback window using the method in our post on renewing legacy software incrementally, and for keeping data consistent between services, see data synchronisation in integrations.
Need help with this topic?