Monolith vs Microservices for a Small Team: Why the Monolith Usually Wins
Cloud

Monolith vs Microservices for a Small Team: Why the Monolith Usually Wins

Not a migration story — a mechanism argument. Every microservices pitch quietly asks a small team to trade function calls for network calls, and pay for that trade in local dev complexity and observability tooling before there's a team boundary that actually needs it.

Published November 4, 202510 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

Why does a monolith usually beat microservices for a small engineering team, mechanically rather than as a matter of taste?

Because splitting an application into services doesn't remove complexity, it relocates it — from something the compiler and a debugger already handle (a function call, a transaction, a stack trace) to something the team now has to build and operate by hand (a network call with its own failure modes, a distributed transaction with no free atomicity, a trace that has to be stitched back together across service boundaries). At small scale that trade is a clear loss: local development gets harder because you're now running and coordinating several services instead of one process, observability tooling that used to be optional becomes mandatory just to answer 'what broke,' and none of it buys anything until team size crosses the point — roughly where Conway's law starts to bite — where separate services actually reduce coordination cost instead of adding to it.

  • A function call is atomic, synchronous, and fails in ways your language's error handling already covers; a network call between services can partially fail, time out, retry into duplicate side effects, or succeed on one side and not the other — none of which existed as a category of bug before the split
  • A single-database transaction gives you real ACID guarantees for free; splitting the same operation across two services means either accepting eventual consistency and building compensating logic (sagas), or building a distributed transaction coordinator that most teams have no business maintaining
  • Local development complexity scales with the number of services a change touches — a monolith's 'run the app' becomes 'run five services, their databases, and the network between them,' often via a docker-compose file nobody fully remembers the shape of
  • Observability that's optional in a monolith (a stack trace is already a trace) becomes mandatory infrastructure with microservices — distributed tracing, correlation IDs threaded through every call, and log aggregation across services just to answer questions a debugger used to answer for free
  • The threshold where separate services start to help is a team-size and coordination problem, not a technical one — Conway's law means service boundaries pay off once they let separate teams ship independently without stepping on each other, which requires enough people to actually have separate teams

Evidence notes

Microservices vs. Modular Monolith for a 10-Person Team

A companion post arguing the practical middle ground — a well-structured modular monolith — for teams at this scale; this post argues the mechanism underneath why that trade holds, rather than repeating the modular-monolith pattern itself.

Continue with purpose

The pitch for microservices at a small company almost never starts from a measured problem. It starts from a conference talk, or a job posting that lists Kubernetes and gRPC as requirements, or a vague sense that a monolith is something you're supposed to have already outgrown. What it doesn't start from, usually, is a specific coordination cost that's actually hurting the team today. That's worth noticing, because the argument for microservices is a real one — it just isn't an argument about code quality or scalability in the abstract. It's an argument about organizations, and most small engineering teams aren't the organization it's an argument for yet.

What does splitting a monolith actually replace, mechanically?

A function call. That's the unglamorous truth underneath every microservices split: somewhere in your monolith, code that used to call another function in the same process now has to call that logic over the network instead, because it moved to a different service. And a function call and a network call are not the same operation wearing different clothes — they fail in entirely different categories of way. A function call either returns or the whole process crashes; there's no in-between state to reason about. A network call can time out after the remote side already committed its work. It can succeed on retry and duplicate a side effect that already happened once. It can return an error that means "definitely didn't happen" or an error that means "no idea if it happened," and your code has to know which, because retrying the wrong one is how a customer gets charged twice. There is a whole operational layer above monolith vs microservices for a small team that XenGrowth's marketing operations practice documents.

Operation

As a function call (monolith)

As a network call (services)

Failure mode

Exception, caught synchronously, process state is known

Timeout, partial failure, or ambiguous success/failure — state on both sides may disagree

Retry safety

Not usually a question — it either ran or it didn't, once

Requires idempotency keys or the retry can duplicate the effect

Latency

Nanoseconds to microseconds, effectively free

Milliseconds at best, and now on the critical path of every caller

Debugging a failure

A stack trace, in one process, in one log

Correlating logs across two or more services, often with clocks slightly out of sync

None of this means the network call is wrong to introduce — sometimes it's exactly the right boundary. It means every service split is quietly asking the team to take on a harder category of failure in exchange for something, and the something has to be worth more than what got harder.

What happens to a transaction that used to be one database commit?

This is where the mechanism argument gets sharpest. Inside a single Postgres database, an operation that touches three tables — debit an account, create an order, decrement inventory — wraps in one transaction and either all of it commits or none of it does. That's ACID, and it's free: the database gives you that guarantee whether or not you ever think about it. Split those three concerns into three services with three databases, and that guarantee is gone. There's no free atomicity across a network boundary. You're left with two real options, and both cost something a single transaction didn't: eventual consistency, where you accept that for some window the order exists but inventory hasn't decremented yet, and you build compensating logic (a saga) to undo the order if the inventory step later fails; or a distributed transaction protocol, which is real, well-studied, and genuinely not something most small teams should be maintaining themselves, because getting it wrong is worse than not having it. For the the operations side of this angle, see The XenGrowth resource library.

A small team rarely has the headcount to build sagas correctly for every cross-service operation, which means in practice a lot of split-too-early architectures end up with inconsistency bugs nobody designed on purpose — an order that exists with no matching inventory decrement, discovered three weeks later by a confused support ticket instead of a failed test.

Why does local development get so much harder?

In a monolith, "run the app" means one command starting one process against one database. Every engineer's laptop can do that trivially, and a new hire is productive on day one. Split that same app into five services and "run the app" means running five processes, their five databases (or a shared one, which reintroduces its own coupling), and the network between them — usually via a Compose file that grows a service at a time until nobody fully remembers why one of them exists or what happens if you start them in the wrong order. Debugging a change that crosses two services means running both, often against data that has to be seeded consistently across two databases, which is its own maintenance burden nobody budgeted time for.

  • A monolith's local setup is bounded by the app itself — one process, one database, one command

  • A split system's local setup is bounded by however many services a given change touches, which grows over time as the service count grows

  • Debugging a cross-service bug requires reproducing the interaction between services locally, not just the one service where the symptom shows up

  • New engineer onboarding time scales with this same complexity — a monolith's onboarding doc fits on one page for a reason

What happens to testing when the logic you're testing lives in three places?

A monolith's integration test calls a function and asserts on the result — the whole call graph runs in-process, so the test is fast and deterministic by default. Split the same logic across services and an equivalent test either has to stand up multiple real services (slow, flaky, and expensive to run on every commit) or mock the service boundary (fast, but now testing a contract you're maintaining by hand instead of the real interaction). Neither is wrong, but both are new categories of test infrastructure a monolith never needed: contract tests to catch a producer and consumer silently drifting apart, and either a docker-compose-based integration environment or an investment in consumer-driven contract testing tooling most small teams have never had to build before. The security surface grows the same way, quietly: every call that used to be an in-process function invocation is now a network request that has to authenticate itself, which means service-to-service auth, and often mutual TLS, becomes a real system to design and operate rather than something the process boundary handled implicitly for free. If AI agents and marketing automation is the part you are stuck on, XenGrowth on governed AI marketing workflows is the better reference.

Dimension

Monolith

Split into services

Integration testing

In-process function calls; fast, deterministic, no test infrastructure beyond the app itself

Requires either running real dependent services or maintaining mocked contracts that can drift from reality

Deployment unit

One artifact, one deploy, one thing to roll back

N artifacts, N deploy pipelines, and a rollout order to reason about when they depend on each other

Security surface

One process boundary; internal calls aren't a network-exposed attack surface

Every inter-service call is a network call, meaning auth, TLS, and network policy between services you didn't need before

On-call cognitive load

One system to hold a mental model of

A mental model of N systems plus the contracts and failure modes between them

Where a bug can hide

Anywhere in one codebase, but one debugger sees all of it

Anywhere in N codebases, and some bugs only exist in the interaction between two of them

Why does observability go from optional to mandatory?

In a monolith, a stack trace already is a trace. One request, one process, one call stack — when something breaks, the log line and the exception traceback tell you exactly what called what, in order, for free. You can genuinely run a small monolith's early life without much observability tooling at all, because the debugger and a decent logger cover most of what you need. Split the same request across three services and that stack trace stops existing as a single artifact. You now need a correlation ID threaded through every service so log lines from different processes can be stitched back into one story, distributed tracing to see where time actually went across the hop, and log aggregation so "what happened to this request" is answerable at all instead of requiring someone to manually cross-reference three services' logs by timestamp. None of that is exotic tooling anymore — but it's tooling a monolith simply didn't need to answer the same question, and setting it up and keeping it correct is real, ongoing work for a team that could have spent that time on the product.

Microservices don't remove complexity from a system. They move it out of the process, where a debugger could see it, and into the network, where you now need a whole observability stack just to see the same thing.

So when do service boundaries actually start helping instead of hurting?

This is where Conway's law stops being a slogan and becomes the actual mechanism worth tracking. Conway's law says a system's architecture ends up mirroring the communication structure of the organization that built it — and the useful reading of that isn't "it's inevitable," it's "so pick the org structure and let the architecture follow it on purpose." A single team of six or eight engineers is one communication structure: everyone can plausibly know the whole codebase, and a Slack message resolves most coordination questions. Split that same six or eight people into three teams each owning a service, and you've created communication overhead — API contracts between teams, versioning, release coordination — that didn't exist when it was one team talking in one channel about one codebase. XenGrowth on building one SEO and GEO content system goes further into AI search, GEO and discovery.

Service boundaries start paying for themselves once there are actually separate teams whose independent shipping speed is worth more than the coordination cost of maintaining contracts between their services — which means enough people, doing different enough things, that forcing them into one shared codebase and one shared deploy is itself the bottleneck. That's a headcount and org-structure threshold as much as a technical one, and it arrives later, for most companies, than the architecture diagrams from a much larger company's engineering blog make it look.

  1. You have multiple teams, not multiple engineers on one team, and they're routinely blocked on each other inside a shared codebase

  2. A specific part of the system has scaling or reliability requirements different enough from the rest that coupling its deploys to everything else is the actual problem

  3. You can name the API contract between the proposed services concretely, not just the org chart line you're drawing on a whiteboard

  4. You're prepared to build and maintain the operational cost this post walks through — network failure handling, consistency strategy, local dev tooling, observability — because it's cheaper than the coordination cost you're solving

  5. If you can't check all four, the monolith isn't a compromise. It's the correct architecture for where the team actually is

None of this is an argument that microservices are wrong in general — it's an argument that the trade only makes sense once there's an organizational reason to actually pay for it, and most small teams asking this question haven't reached that reason yet. If you want the practical pattern for structuring a monolith so it doesn't calcify into the thing microservices were meant to fix, microservices vs. modular monolith for a 10-person team covers that directly. And if the orchestration question is what's actually driving this conversation rather than the application architecture itself, that's a separate question worth answering on its own terms.

Further reading from XenGrowth

Where this work meets go-to-market

The operational playbooks that sit alongside monolith vs microservices for a small team live with XenGrowth's marketing operations practice.

What does splitting a service actually cost?

Four questions on what changes the moment a function call becomes a network call. The post's argument is that the costs are real and mostly invisible until you've paid them.

1 / 4
You extract a module into its own service. What has fundamentally changed about calling it?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

MicroservicesMonolithSoftware ArchitectureConway's LawDistributed Systemscloud

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

Event-Driven Architecture Patterns for Startups That Actually Need Them

Event-driven architecture solves real problems. Most startups adopt it to solve problems they don't have yet — here's how to tell which camp you're in.

Navigate

How I Self-Host PostgreSQL for My SaaS (and When I Wouldn't)

Running Postgres in a container is easy. Running it in a way that survives a redeploy, a full disk, and an eventual major-version upgrade is the actual job. Here's the setup, tuned against Postgres's own defaults, and the honest list of where managed wins outright.

Navigate

10 Mistakes That Break a Self-Hosted SaaS

None of these ten show up as a single dramatic outage. They show up as a disk that quietly fills, a rollback that turns out to be impossible, a backup nobody ever restored. Here's the mechanism behind each one, and the fix.

Navigate

Cloudflare R2 vs S3 vs MinIO for SaaS File Storage

The storage price per gigabyte is close enough across all three that it barely matters. Egress is where the decision actually gets made — and one of these three had a rough 2026 that changes the self-hosting math entirely.

Navigate

The Backup Strategy Every Self-Hosted SaaS Needs (3-2-1, Applied)

3-2-1 is easy to nod along to and easy to get wrong in the specific way that only shows up on the day you need it. Here's what it actually means for one VPS running Postgres and Docker volumes, not the generic version you've already skimmed past twice.

Navigate

Managed Postgres vs Self-Hosted: Supabase, Neon and a VPS Compared

Supabase bills you for compute whether anyone's querying it or not. Neon bills you almost nothing until someone is, then charges for the second it takes to wake up. A VPS charges you the same either way and hands you every operational job both of the others do for you. None of these is the right answer by default.

Navigate

My Rule for Deciding What to Self-Host and What to Keep Paying For

Self-hosting everything is a bad idea, and I can point to the exact service where I decided that on purpose. Here's the actual rule I use, not a survey of options — and the one counterexample that explains why the rule exists.

Navigate

My Complete Self-Hosted Stack for SaaS in 2026

Contabo, Coolify, Docker, Cloudflare, Postgres, R2, Resend, Uptime Kuma, Turborepo and Docker Hub. Here's every piece of the stack I actually run, what each one replaced, and why I picked it over the alternatives.

Navigate

The Simplest Production Architecture for a Bootstrapped SaaS

One box, a handful of managed pieces around the edges, and a very short list of things you're not allowed to build yet. Here's the architecture, priced out to $20 a month, and the exact signal that tells you when to add each thing you skipped.

Navigate

How I Secure a Fresh VPS Before Deploying Anything

A brand-new VPS gets scanned within minutes of getting an IP address. Here's the exact order I run through before a single container touches the box — and which of these steps are real protection versus which ones are just theatre.

Navigate