Your team isn't big enough for microservices: stay monolithic

6 min read

A London office whiteboard with sticky notes mapping a modular monolith architecture

Your team isn't big enough for microservices. If you have fewer than 30 engineers, a single product domain, and a release cadence measured in days rather than minutes, splitting into services will cost you velocity, reliability, and the will to live. Stay monolithic until the monolith genuinely hurts, and even then, prefer a modular one. The microservices architecture is a resume-driven design mistake when applied to teams that haven't earned the operational tax it demands. This post is a working dev's checklist for knowing when to resist the split.

The hidden headcount tax of every service

People count services in endpoints. They should count them in humans. Sam Newman has been honest about this for years: the canonical "you need microservices" team size from his own writing starts around 50 engineers, and even that assumes platform tooling already exists. Below that line, every new service adds at least 0.25 FTE of glue work: someone owns the deploy pipeline, the CI config, the service catalog entry, the on-call rotation slot, the schema registry, and the versioned client. Two services in one repo used by three teams has tripled your operational surface area. Three engineers haven't been hired to do that. They've been hired to ship features.

Look at the UK shops that ship quickly without the distributed systems tax. Monzo famously runs over 1,600 microservices now, but they also have a 300+ strong platform team and a regulator breathing down their neck. GOV.UK's Verify team scaled to multiple services with explicit platform engineering investment from CDIO budgets the average startup doesn't see. Compare that to a 12-person SaaS team in Manchester I worked with last year: they split "users" and "billing" into two services to "decouple deploys" and spent the next quarter fighting shared-schema drift, dual CI, and dual observability stacks. They merged back. Velocity returned.

The rule of thumb I use: count your platform engineers, not your total engineers. If you don't have at least three people whose full-time job is CI/CD, observability, and on-call hygiene, you don't have a microservices team. You have a monolith with extra network hops.

The modular monolith is the answer you keep skipping

The real fix for a large codebase isn't to split the deploy unit. It's to split the code. This is what the "modular monolith" pattern is for, and it's what most UK teams should actually adopt. One deployable artifact, strict internal module boundaries enforced by your language, no cross-module database tables, and explicit public APIs per module. You get most of the cognitive benefits of microservices (clear ownership, blast-radius discipline, easier onboarding) without paying the network, deployment, or data-consistency tax.

Python makes this clean with package-level discipline. Django can ship a single WSGI app where each "app" directory is a module with its own models, views, and a deliberately small `__init__.py` public surface:

# monolith/orders/__init__.py
from .public import create_order, get_order, list_orders_for_user
from .internal import _recalculate_totals  # noqa: F401

TypeScript/JavaScript does this with subpath exports and a single Next.js or NestJS app:

{
  "name": "@monolith/billing",
  "version": "1.0.0",
  "main": "./dist/index.js",
  "exports": {
    ".": "./dist/public.js",
    "./internal": "./dist/internal.js"
  }
}

Now you've got a code-level seam you can later extract into a service when the deploy pain actually shows up. BBC iPlayer's public talks have repeatedly emphasised this approach: one deployable, many modules, clear ownership, and a rule that any cross-module call goes through the public API. NHS Digital's reference architectures lean the same way. You don't need a service boundary to get architectural discipline. You need a code review rule.

What the boundaries actually buy you

Module boundaries stop three specific classes of bug: (1) another team reading your private tables directly, (2) one team hot-patching another team's data to "fix" a bug, and (3) implicit ordering assumptions that only surface when one service scales and the other doesn't. None of those require a separate process or a network call to enforce. They require a `from .public import` rule and a linter that fails the build if you bypass it. Add a per-module owner in your `CODEOWNERS` file and you've replicated the "single owner per service" benefit at a fraction of the cost.

When splitting actually pays off (and how to know)

Microservices aren't wrong. They're premature. The legitimate triggers are concrete and measurable, not vibes:

  • Different scaling profiles. Your reporting worker chews 16 cores and 32GB while your user-facing API needs 50 tiny replicas. Now you have a reason.
  • Hard compliance isolation. HMRC-grade workloads where PCI data physically can't touch card data in the same process. This is a real reason, not an excuse to use Kafka.
  • Genuinely independent deploy cadences. Team A ships 20 times a day, Team B ships once a month, and they keep stepping on each other. A repo split or service split fixes that.
  • Polyglot by necessity. Your ML team genuinely needs Python on GPU boxes and your API team needs Go on ARM, and you can't share a deploy unit.

If you can read those four points and none of them describe your situation, your reason for going distributed is "we read about it on Martin Fowler's blog." That's not engineering. That's fashion. The HFT shops in the City and the streaming teams at the BBC have legitimate reasons. A 10-person startup building a B2B SaaS for dentists in Bristol does not.

Here's a quick diagnostic you can run this afternoon. Pull your last 90 days of incidents. Count how many were caused by the monolith being too coupled to deploy safely versus how many were caused by cross-team coordination. If the answer is "coupling," that's a code-organisation problem. Refactor the modules. If the answer is "different scaling," you're a real candidate.

What to do instead this quarter

Take the time you'd spend splitting services and put it into the things that actually move the needle for a small UK team: faster CI, better local dev, real feature flags, and proper observability. A 20-minute test suite beats a service mesh. Native binaries for local dev beat a 12-container docker-compose. A single Postgres with a `pg_stat_statements` extension and a sane `work_mem` beats introducing Kafka because someone on the team watched a Confluent talk.

Then draw your module boundaries in the same repo, give each module an owner, enforce the public/internal split with a linter, and write the seam clean enough that extracting a service later is a one-week job, not a six-month re-platforming. That's the path. Monolithic architecture isn't the legacy choice. It's the grown-up one. Microservices are what you graduate to when the monolith genuinely hurts, not what you adopt to avoid the hard work of designing modules.

FAQ

How big does my team need to be before microservices make sense?

Roughly 40-50 engineers with at least 3-5 dedicated platform engineers, plus real investment in CI/CD, observability, and on-call tooling. Below that, the operational tax exceeds the autonomy benefit.

What's the difference between a modular monolith and a distributed monolith?

A modular monolith enforces module boundaries in code and at deploy time, with one runtime. A distributed monolith pretends to be microservices but couples services tightly through shared schemas, sync HTTP calls, and 2PC transactions. The latter is strictly worse than either option.

Can I migrate from a modular monolith to microservices later?

Yes, and that's the point. A modular monolith with clean module boundaries is the cheapest source for extracting services later. You do the extraction once per module that genuinely needs to scale or deploy independently, instead of upfront across the whole system.