Splitting an application into microservices makes sense when the system has grown beyond what one team can manage, when its parts change at very different speeds, or when they need to scale independently. For small and mid-sized teams, a single well-modularised application, a modular monolith, is often the better choice. And when splitting is the right call, you do not rewrite the system in one go; you move it piece by piece while it keeps running.
Microservices are often talked about as a destination in their own right, but every architecture has costs. Below we compare the three approaches, look at what microservices really cost and describe how to migrate safely.
Monolith, modular monolith and microservices: what is the difference?
A monolith keeps all functionality in one codebase and deploys it as a single unit. A 2019 post from Shopify Engineering lists its advantages: it is the simplest architecture to build and lets you ship early; there is one repository, one test and deployment pipeline and one database; and components call each other directly rather than over the network.
The trouble starts when there are no internal boundaries. Shopify describes how its shipping-rate code lived alongside the checkout code, with very little stopping the two from calling each other. Over time, everything gets connected to everything else.
A modular monolith, in Shopify’s definition, is a system where all the code powers a single application but strictly enforced boundaries exist between domains. It still deploys as one unit; the difference is that modules such as orders, payments and inventory only use each other’s defined interfaces, not their internals. Shopify, which describes its codebase as one of the largest Ruby on Rails codebases in existence, chose to evolve into a modular monolith rather than break up into microservices.
Microservices, in James Lewis and Martin Fowler’s 2014 description, mean designing an application as a suite of independently deployable services. Services are organised around business capabilities such as ordering or invoicing, talk to each other over the network and usually each manage their own database.
| Aspect | Monolith | Modular monolith | Microservices |
|---|---|---|---|
| Deployment | Single unit | Single unit | Each service separately |
| Data | Shared database | Shared database, ownership per module | Each service owns its data |
| Calls between modules | Direct, unrestricted | Direct, through defined interfaces | Over the network |
| Operational overhead | Low | Low | High |
| Good fit | Small team, early-stage product | Growing product, one or a few teams | Many independent teams, different scaling needs |
The real costs of microservices
Fowler calls the extra overhead the “microservice premium”: automated deployment, monitoring, failure handling and eventual consistency all take effort and can slow development down considerably. That cost shows up in four places.
Operations
Instead of one application, you deploy, version and scale many services separately. Fowler’s article on microservice trade-offs stresses that running lots of services requires a mature operations team and a DevOps culture.
Distributed data
Saving an order and reducing stock is a single database transaction in a monolith. Split it across two services and it gets hard. In the database per service pattern described by Chris Richardson, transactions that span services are handled with techniques such as sagas, and queries that span services with API composition or CQRS. All of that is extra design and code. Remote calls are also slow and can fail, and keeping data strongly consistent is much harder in a distributed system.
Observability
An error is no longer in one log file but spread across the services a request passed through. Without central logging, metrics and distributed tracing, finding the cause is hard. In OpenTelemetry’s terms, a trace shows the end-to-end path of a request across services and machines, and setting that up and running it is a job of its own.
Team structure and Conway’s law
Conway’s law, stated by Melvin Conway in 1968, says that a system’s design mirrors the communication structure of the organisation that builds it. Microservices work well with small, long-lived teams that have every skill a business capability needs. A single team of six running twenty services is an architecture that does not match the organisation: everyone touches every service, yet every service has to be operated on its own.
Signals that it is time to split
Fowler’s advice is clear: do not consider microservices unless the system is too complex to manage as a monolith. He lists large teams, multi-tenancy, business functions that need to evolve independently and scaling as sources of that complexity. In practice, these are the signals we look for:
- Teams wait on each other, and one team’s change blocks another team’s release.
- One part has a very different load profile from the rest, such as heavy report generation or image processing, and scaling it separately brings a measurable gain.
- Business areas change at very different speeds; one ships several times a week, another a few times a year.
- One part has clearly different security or compliance requirements.
- Deployments are so slow and risky that the team avoids releasing.
Some signals are misleading. “The code is a mess” is not a reason to split on its own: break up a monolith without clear boundaries and you get the same mess, now talking over the network. And before saying “we can’t scale”, measure where the bottleneck is; sometimes a database index or a caching change is enough.
Prerequisites: CI/CD, automated tests and monitoring
Fowler’s microservice prerequisites are three basics: being able to provision a new server within hours, basic monitoring that detects serious problems quickly, and a deployment pipeline that gets services to test and production within a couple of hours. A culture of close collaboration between development and operations has to come with them.
We add automated tests to that list. To deploy one service on its own, you need to verify automatically that a change does not break its contract (API) with other services. Without these foundations, moving to microservices just splits your existing problems into more pieces. Order matters too: first the CI/CD pipeline, tests and monitoring, then the first service.
Migrating step by step with the strangler fig pattern
Rewriting the system from scratch and switching over in one night is tempting, but Fowler writes that he has seen this simple-sounding plan go down in flames most of the time. His alternative is the strangler fig pattern, named after the figs he saw in the Queensland rainforest in 2001, which germinate in a nook of a host tree and gradually grow around it. The new system grows around the old one, functionality moves across piece by piece, and the old system is retired over time.
- Modularise the monolith first. Make the boundaries in the code explicit and stop modules from reaching into each other’s tables. According to Fowler’s Monolith First, a monolith with clear boundaries at both the API and the data level is much easier to split later.
- Pick the first piece. Start with a module that has few dependencies, a clear boundary and a concrete benefit from being separated. Small pieces carry less risk.
- Put a routing layer in front. An API gateway or reverse proxy gradually sends the relevant requests to the new service; if something goes wrong, traffic can go back to the old code.
- Move the data. The new service becomes the owner of its data. During the migration you may need temporary measures such as keeping both sides in sync; Fowler calls these “transitional architecture”.
- Remove the old code and measure. Delete the old implementation of what you moved, check whether the expected benefit arrived, then move to the next piece.
Data ownership
Data is the most underestimated part of a migration. In the database per service pattern, each service’s data belongs to it alone, and others reach it only through its API. Two services reading the same tables directly are not really separated, because a schema change in one breaks the other. Write down which service owns which data at the very start of the migration plan.
Frequently asked questions
Should we start a new project with microservices?
In most cases, no. Fowler observes that almost all successful microservice stories started with a monolith that grew too big and was broken up, while systems built as microservices from scratch often ended up in serious trouble. Drawing service boundaries correctly from day one is hard even for experienced teams who know the domain well.
How big does a team need to be for microservices?
There is no fixed number; Fowler also notes that service counts and team sizes vary widely in real systems. A useful test: every service should have a clear owner who deploys it, monitors it and responds when it breaks. If you have more services than teams able to own them, you have probably split too early.
Conclusion
Microservices are valuable in systems with many independent teams and different scaling needs, but they bring a real cost in operations, data, observability and team structure. For small and mid-sized teams, a well-modularised monolith is often the better choice. When splitting is needed, putting CI/CD, tests and monitoring in place first and then moving piece by piece with the strangler fig pattern keeps the risk down.
If you are not sure whether your system needs to be split, take a look at our software architecture and DevOps page or get in touch. We can make the call together, based on your team size and your system’s load.