We Merged Six Microservices Back Into a Monolith

A 20-person team was running 14 microservices with three full-time engineers just keeping the infrastructure alive. We consolidated six of them into a modular monolith and cut their deploy time by 70%.


Nobody wants to be the person who suggests going back to a monolith. It feels like admitting defeat, like proposing that the team trade in their sports car for a minivan. But sometimes the minivan is what gets everyone to school on time.

I joined a client engagement in March — a B2B SaaS company with about 20 engineers, building a logistics platform. They had 14 microservices. Fourteen. For a product with roughly 6,000 daily active users and a single-digit number of enterprise customers.

The CTO had started the microservices migration two years earlier, right after reading a blog post about how Amazon does it. The reasoning was familiar: independent deployability, team autonomy, technology flexibility. All the right words.

The reality was different.

What the Architecture Actually Cost Them

Three engineers — 15% of the entire engineering team — spent most of their time on infrastructure. Not building features. Not fixing bugs. Maintaining Kubernetes manifests, debugging inter-service networking, updating Helm charts, and keeping 14 separate CI/CD pipelines from drifting apart.

Deployments took 45 minutes on a good day. A change that touched the order service and the billing service required coordinating two separate PRs, two separate deploys, and hoping nobody shipped something in between that broke the contract. They had integration tests, but the test environment was so flaky that engineers had learned to just click "re-run" until it passed.

The latency numbers told their own story. A single API call from the frontend — say, loading a shipment detail page — fanned out to five internal service calls. Each hop added 15-40ms of network overhead. The p95 for that page was 1.8 seconds. For what was essentially a database read with some business logic on top.

Note

Not every team that adopts microservices is making a mistake. But the benefits of microservices scale with team size and organizational complexity. A 200-person engineering org with autonomous teams shipping to millions of users is a very different situation than a 20-person team with a shared backlog.

The Conversation Nobody Wanted to Have

When I presented my assessment — that they should consolidate most of their services — the room went quiet. The senior engineers had invested serious effort in the current architecture. The CTO had championed it. Nobody was excited about what felt like moving backward.

So I reframed it. This wasn't about monolith versus microservices. It was about matching the architecture to the team. I pulled up their git history. Of the 14 services, eight were maintained by the same three people. Six of them shared a deployment cadence — they almost always shipped together anyway. Four had identical technology stacks, identical dependency versions, and nearly identical Dockerfiles.

These weren't independent services. They were a monolith that paid a distributed systems tax.

What We Actually Did

We didn't merge everything. The notification service stayed separate — it had genuinely different scaling characteristics and a dedicated maintainer. So did the async job processor and the external API gateway. Those boundaries made sense.

But the core domain — orders, billing, inventory, shipment tracking, customer management, and routing — got consolidated into a single deployable unit. A modular monolith with clear internal boundaries.

The migration took about six weeks. The key decisions:

Shared database, separate schemas. Each module kept its own schema. No cross-module table access. If billing needed customer data, it went through the customer module's public API — just a function call now instead of an HTTP request.

Module boundaries enforced by convention and linting. We set up architecture tests using ArchUnit that failed the build if one module imported internals from another. Not as strong as a network boundary, but strong enough for a team of 20.

One CI/CD pipeline instead of six. Build time actually went down because we weren't spinning up six separate Docker images and running six separate integration test suites. One build, one artifact, one deploy.

// Before: HTTP call with error handling, retries, circuit breaker
const customer = await customerServiceClient.getCustomer(customerId, {
  timeout: 3000,
  retries: 2,
  circuitBreaker: billingCircuitBreaker,
});
 
// After: function call
const customer = await customerModule.getCustomer(customerId);

That code change repeated across hundreds of call sites. Every one of them deleted error handling for network failures that no longer existed.

The Numbers After Eight Weeks

Deploy time dropped from 45 minutes to 12 minutes. The p95 on that shipment detail page went from 1.8 seconds to 340ms — most of the improvement was just eliminating network hops between services that ran on the same machine anyway.

The three engineers who had been doing infrastructure work full-time got two of those engineers back on product work. They still needed one person managing infrastructure, but for five services instead of fourteen.

The flaky integration tests? Most of them disappeared. It turns out that 80% of the flakiness was network-related — timeouts between services in the test environment, containers starting in the wrong order, port conflicts. With in-process calls, those failure modes simply didn't exist anymore.

What I Got Wrong

I underestimated the migration time for the data layer. The services had drifted in how they handled timestamps, currency values, and null fields. The orders service used UTC everywhere. Billing used the customer's local timezone. The customer service had a mix of both, depending on which engineer had written the endpoint. Reconciling those inconsistencies added two weeks to the timeline.

I also assumed the team would immediately feel better about the change. They didn't — not at first. There was a grieving period for the microservices architecture. Engineers who had become experts in service mesh configuration and distributed tracing felt like their skills were being devalued. That's a real cost and I should have addressed it earlier.

When to Actually Use Microservices

I'm not anti-microservices. I've helped teams decompose monoliths when it was the right call — usually when they had 50+ engineers, multiple teams with genuinely different deployment cadences, and services with meaningfully different scaling requirements.

But for the majority of teams I've worked with — teams under 30 people, with shared codebases and synchronized releases — a well-structured monolith is going to serve them better. The industry spent a decade treating microservices as the default. That pendulum is swinging back, and I think it's a healthy correction.

The question worth asking isn't "should we use microservices?" It's "what problem would decomposition solve that we can't solve with better module boundaries in a single codebase?" If the answer is vague, you probably don't need the network boundary.