This website uses cookies

Read our Privacy policy and Terms of use for more information.

A team I talked to last year was proud of their migration. They had taken a Rails monolith and carved it into thirty-one services. Clean repos, one per service, each with its own Dockerfile. On the whiteboard it looked like the reference diagram from every conference talk.

Then I asked how they shipped a change. The answer: a release train every Thursday, where all thirty-one services get tagged, built, and deployed together, in a dependency order maintained by hand in a wiki page. If service 12 fails its smoke test, the whole train rolls back. One shared Postgres instance sits behind all of them, and half the services read tables that another service writes.

That is not a microservices architecture. It is a monolith that someone ran through a wood chipper. It has every cost of distribution - network hops, partial failures, serialization, distributed tracing - and none of the benefit the pattern was supposed to buy. And it is the single most common outcome when a team adopts microservices, because the industry copied the wrong part of the pattern. The thing that makes microservices work is not that the services are small. It is that they are independently deployable. That property is organizational before it is technical, and you cannot get it by splitting your repos.

What the pattern actually said

When James Lewis and Martin Fowler wrote the canonical microservices article in 2014, they did not define the style by service size. They gave it a checklist of characteristics, and the load-bearing one is stated in the first paragraph: services are "independently deployable by fully automated deployment machinery." The rest of the checklist - componentization via services, organized around business capabilities, decentralized data management, smart endpoints and dumb pipes - all exists to protect that one property.

Read those characteristics as a set of constraints and a picture emerges. Decentralized data management (each service owns its own database) is not there for scaling. It is there so that service A can change its schema without coordinating with service B. Business-capability boundaries are not there for cleanliness. They are there so that a single team can own a service end to end and ship it without a cross-team meeting. Every item on the list is in service of the same goal: a team should be able to deploy its piece on its own schedule, without lockstep coordination with anyone else.

Miss that, and you have missed the pattern. You can have thirty services and zero independent deployability. You can also have three services and full independent deployability, which is usually the better trade. The number of services is an implementation detail. The deploy boundary is the pattern.

The tell is the shared database

If you want to know in five minutes whether a system is really microservices or a distributed monolith, do not count the services. Look at the database. One shared schema that multiple services read and write is the clearest signal that the deploy boundary is a fiction.

Here is why it is fatal. Suppose the orders service owns this table, and the billing service reads it directly for a nightly reconciliation job:

-- owned by "orders", read directly by "billing"
CREATE TABLE orders (
  id           bigint PRIMARY KEY,
  customer_id  bigint NOT NULL,
  total_cents  integer NOT NULL,   -- billing depends on this name and type
  status       text NOT NULL
);

The moment billing runs SELECT total_cents FROM orders, the two services are no longer independent. If the orders team wants to rename total_cents to total_amount, or move money to a numeric type, or split the column into subtotal and tax, they cannot. Not without finding every other service that touches the table, coordinating a synchronized migration, and deploying both services together. The database has quietly welded the two services into one deployable unit. The repos are separate; the release is not.

Real microservices treat the database as private the way a class treats its fields as private. The only supported way for billing to learn an order's total is to ask orders through its API or to consume an event it publishes. That indirection feels like overhead, and it is, but it is the overhead that buys the independence. Skip it and you are paying distribution's tax while still filing a joint deploy return.

Synchronous call chains are the same anti-pattern wearing a different hat. If a single user request fans out through gateway -> orders -> inventory -> pricing -> tax and every hop is a blocking HTTP call, you have built a monolith whose function calls happen to cross the network. Latency is now additive, every service in the chain has to be up at the same time, and a slow tax service takes down checkout. You have made your call stack distributed without making your deploys independent, which is the worst of both.

Conway's law is not a suggestion

The reason teams get this wrong is that independent deployability is downstream of org structure, and you cannot refactor your org with a Dockerfile. Melvin Conway's 1968 observation - that a system's design mirrors the communication structure of the organization that built it - is the part of the microservices story that gets quoted in slides and ignored in practice.

If three teams share ownership of the checkout flow, and any meaningful change requires all three to agree, then no amount of service-splitting will give you independent deploys. The coordination that used to live in a shared codebase now lives in Slack threads and release-train wiki pages. You did not remove the coupling. You moved it from the compiler, where it was cheap and visible, to human meetings, where it is expensive and invisible. Fowler makes exactly this point in MonolithFirst: almost every successful microservices story started as a monolith that grew too big and was broken up along boundaries the team had already learned, and almost every system built as microservices from day one ended up in trouble, because the team drew the boundaries before they understood the domain.

The corollary is uncomfortable. Microservices are a solution to an organizational scaling problem: you have too many engineers to fit in one codebase without them constantly stepping on each other. That is a real problem, and above some team count it is the dominant one. But if you have fifteen engineers and one product, you do not have that problem yet. Adopting microservices to solve it is like buying a second warehouse because your desk is messy.

What it looks like when it goes wrong in production

You do not have to take this on theory. Two of the most-cited engineering post-mortems of the last decade are both stories of a team copying the topology and then walking it back.

Segment split its data-integration pipeline into a service per destination and ended up with more than 140 microservices. The result was not faster shipping. It was three full-time engineers whose job was just keeping the fleet alive, a shared library that required deploying 140-plus services to update, and test suites that took an hour. They consolidated the whole thing back into a single service called Centrifuge, and the numbers moved the right way: one engineer could deploy in minutes, the shared library got 46 improvements in a year versus 32 in the microservices era, and the operational tax collapsed. Notice what actually hurt them - the fan-out was so wide that a change to shared code forced a lockstep deploy of the entire fleet. They had the topology of microservices and the deploy coupling of a monolith.

Amazon Prime Video's video-quality monitoring team told a version of the same story from the cost angle. Their audio and video defect detector was built as a distributed system of serverless functions orchestrated by Step Functions, passing intermediate frames through S3. It did not scale economically. They rebuilt it as a single process on ECS, moved the data transfer in-memory instead of through S3, and cut infrastructure cost by about 90 percent. The lesson people took from it - "Amazon says monoliths are back" - is the wrong one. The right one is narrower: for a workload that is really one tight data-processing pipeline with no independent teams behind its pieces, splitting it into distributed components bought nothing and cost a lot.

Both cases are the same mistake. The pieces were not owned by separate teams that needed to deploy on separate schedules. So distribution added latency, failure modes, and operational load, and returned no deploy independence, because there was no organizational independence to encode.

Yes, but - microservices absolutely work

None of this means the pattern is wrong. Netflix, Amazon's retail side, Uber, and a long list of others run thousands of services and could not function any other way. When you have hundreds of engineers, forcing them through one deploy pipeline is its own catastrophe: the merge queue becomes the bottleneck, one bad commit blocks everyone, and coordination cost grows faster than the team. At that scale, the ability for a team to own a service and ship it forty times a day without touching anyone else's code is worth every bit of distribution's tax.

The pattern is not the problem. The copy is. Microservices are the correct answer to "we have more teams than one codebase can hold without constant collision." They are the wrong answer to "our monolith is getting hard to understand" - that is a modularization problem, and you can solve it inside one deployable with clear module boundaries and enforced dependencies, which is what a modular monolith is. The failure mode is adopting the answer to a question you do not have yet, and inheriting a distributed system's operational burden as the consolation prize.

What to do on Monday

Stop measuring your architecture by service count and start measuring it by deploy independence. Pick any two services and ask one question: can team A deploy its service to production right now, without coordinating with team B, without a joint migration, without a release train? If the answer is no, those two things are one service that happens to run in two processes, and you should either merge them or fix the coupling - usually the shared database - that is welding them together.

Draw your service boundaries around teams and deploy cadence, not around nouns in the domain or around lines of code. A good default heuristic: one deployable unit per team that needs to ship on its own schedule, and not one more. If you have one team, you have one service, and it should be a modular monolith with disciplined internal boundaries so that when you do need to split, the seams are already there. Fowler's monolith-first advice is not nostalgia. It is the recognition that you cannot draw the right boundaries until you have lived in the domain, and the cheapest place to be wrong about a boundary is inside a single codebase where moving it is a refactor, not a migration.

The next time someone proposes breaking the monolith into services, do not ask how small the services should be. Ask which teams will own which services, and whether those teams can already deploy without waiting on each other. If they can, microservices will encode a freedom you already have. If they cannot, you are about to distribute your monolith and call it progress.

Sources

Keep Reading