A team of six engineers decides to build their new product as eight services. The reasoning is familiar: it will scale, teams can work independently, and it is how the large companies do it. Eighteen months later, two of those engineers spend most of their week on deployment pipelines and a third is permanently debugging why the order service and the billing service disagree.
Nothing went wrong in any dramatic way. The team simply paid a set of costs that were designed for organisations ten times their size.
What microservices are actually for
Microservices solve an organisational problem more than a technical one. When a hundred engineers share one codebase, they block each other: merges collide, releases queue and one team's bug takes down another's feature. Splitting into services lets groups ship on their own schedule.
That benefit scales with the number of people. For a team of five to fifteen, the coordination problem it solves barely exists. You can talk to each other. You can agree on a release. The cost, however, arrives in full from day one.
The costs nobody puts in the plan
Every call becomes a network call. Inside a single application, calling a function is nearly free and nearly always works. Between services, the call can time out, fail halfway or arrive twice. Each boundary needs timeouts, retries, idempotency and a decision about what to do when the other side is down. That is real design work, repeated for every pair of services that talk.
Transactions disappear. In one database, you can update an order and a payment together and have both succeed or both fail. Across two services with two databases, you cannot. You need sagas, compensating actions and reconciliation jobs. These are solvable problems, and they are also a good deal harder than a database transaction.
Debugging gets slower. A single request now touches five services. To see what happened you need distributed tracing, consistent correlation IDs and logs you can search across all of them. Without those, you are guessing. With them, you have built and must maintain an observability stack.
Deployment multiplies. Eight services means eight pipelines, eight sets of configuration, eight runtime environments and versioning between them. A change that spans two services needs coordinated releases or careful backwards compatibility. Many small teams discover they have built a platform team's workload without a platform team.
Local development degrades. Running the whole system on a laptop becomes a project. New joiners lose days to setup. Tests that need several services are slow and brittle, so people stop running them.
Data gets duplicated. Services own their data, so anything shared is copied or fetched. Copies drift. Teams then spend time deciding which copy is authoritative.
The cost that compounds
These costs are individually manageable. The trouble is that they compete with the product for the same few people. Each hour spent on service mesh configuration or contract versioning is an hour not spent on the thing customers pay for.
Hiring is affected as well. A small team that runs many services needs people who are comfortable with containers, orchestration, networking and tracing as well as the product code. Those people are scarce and expensive, and a team that cannot hire them ends up with operational knowledge held by one or two individuals. When one of them goes on leave, the system becomes a risk nobody else can assess.
Cloud bills grow quietly too. Each service carries its own baseline of compute, its own database instance and its own share of monitoring and logging. Eight small services running all day rarely cost less than one application doing the same work, and the difference is paid for before a single customer arrives.
And the boundaries themselves are guesses. Early in a product, you do not know where the natural seams are. Draw them wrongly and you get a distributed monolith: services that cannot change independently, that must deploy together, and that share a database through the back door. You have all the cost of microservices and none of the benefit.
What we prefer
For most small teams, we recommend a well structured single deployable, often called a modular monolith. One codebase, one deployment, one database, with clear internal modules and strict rules about how they interact.
The discipline is in the boundaries. Each module exposes a small interface and hides its tables. Other modules call the interface, never reach into the data. Dependencies point in one direction. You can enforce this with tooling and code review.
Picture a lending product with modules for customers, loans, repayments and notifications. In a modular monolith, the repayments module asks the loans module for a balance through a defined function. In a service design, the same question is a network request with retries and failure handling. The business logic is identical. Only the second version needs a second deployment, a second database and an on call rota.
This gives you most of the architectural clarity of services with none of the network. And it keeps your options open. A module with a clean boundary can be extracted into its own service later, when you know it needs one.
When splitting does make sense
We are not against services. There are good reasons to split:
- A component has a genuinely different scaling profile, such as an image processing worker that needs many cheap machines.
- It needs a different technology, security boundary or compliance scope. Isolating payment card handling is a common example.
- A part of the system has a separate team that owns it end to end and wants a separate release cadence.
- Its failure must not take down the rest, and isolation is the cheapest way to guarantee that.
Notice that each reason is specific and observable. "We might need to scale" is not on the list.
A test before you split
Ask these questions about any proposed service. Can one person explain what it owns in a sentence? Does it have its own data that nobody else needs to write? Could it be down for an hour without stopping the core product? Would a separate team really run it? If the answers are mostly no, keep it as a module.
Start simple, stay honest
Write the decision down with the reasons, so the next person can tell whether those reasons still hold.
The best sign that you are ready for services is that you can name the specific pain. A deploy that blocks two teams every week, a component that needs ten times the machines of everything else, a compliance boundary an auditor keeps asking about. Pain you can name justifies a split. Pain you only imagine rarely does.
Revisit the decision as the team and product grow. The right architecture for six engineers is rarely the right one for sixty. The mistake is not choosing services or a monolith, it is borrowing a decision made for someone else's size. Build the simplest thing that your actual team can run well at two in the morning, and split when the pain is real and named.
