In 2021, we made a company-wide decree: all new projects start as microservices. By 2024, we were quietly walking that back — not because microservices are bad, but because we'd been using them as a default rather than a decision.

The allure of microservices

It's easy to see why microservices became the default. The benefits are real — independent deployability, language flexibility, fault isolation, and the ability to scale individual services. After years of maintaining a monolith that had become a distributed monolith in disguise, switching felt liberating.

For the first 18 months, it worked beautifully. Smaller services meant cleaner boundaries. Teams moved faster. Deploys went from weekly events to daily occurrences.

"Microservices are not an architecture. They're an organisational pattern. Confuse the two and you'll build the wrong thing for the wrong reasons."

Sam Newman, Building Microservices

Where it started breaking down

The cracks appeared gradually. A "simple" feature — say, updating a user's subscription tier — now required coordinating changes across 6 services. Integration tests became so slow they were routinely skipped. Our service mesh configuration grew more complex than the business logic it was serving.

We ran an internal audit in Q3 2024. The results were humbling:

  • Average time to ship a cross-service feature: 3.4× longer than equivalent monolith work
  • 42% of our on-call incidents were caused by inter-service communication failures
  • Local development setup took new engineers an average of 2.5 days
  • We were running 34 services for a product used by ~12,000 businesses

What we switched to: a modular monolith

We didn't swing back to a spaghetti monolith. Instead, we adopted the modular monolith pattern — a single deployable unit with strict internal module boundaries enforced by linting rules and architectural fitness functions.

project structure plaintext
src/
  modules/
    billing/        → owns its own DB schema slice
    auth/           → zero imports from other modules
    analytics/      → event-driven, async only
    notifications/  → adapter pattern, swappable
  shared/
    events/         → typed domain events only
    utils/          → pure functions, no side-effects

Modules communicate via an internal event bus — no direct imports across boundaries. The discipline required is the same as microservices. The operational complexity is a fraction.

Results after 6 months

The numbers spoke for themselves. Cross-service feature delivery time dropped by 60%. New engineer onboarding dropped from 2.5 days to 4 hours. On-call incidents from inter-service issues: effectively zero.

When should you still use microservices?

We haven't abandoned microservices entirely. They're still the right call when:

  • A component has dramatically different scaling characteristics (e.g. a video processing pipeline vs. a CRUD API)
  • Security isolation is mandated — PCI scope, for example
  • Independent release cadences are genuinely needed across large autonomous teams
  • You're at a scale where the operational overhead is justified (think: 100+ engineers, millions of RPS)

If none of those apply to you today, start with a well-structured monolith. You can always extract a service when the evidence demands it. Going the other way — collapsing microservices into a modular structure — is significantly harder.

The meta-lesson

The real problem wasn't microservices. It was adopting architecture by trend rather than by fit. The best architectural decisions are boring: they're driven by concrete constraints, revisited regularly, and changed when the evidence changes.

We got too enamoured with the pattern and forgot to keep asking: does this still serve us?