Contents

Microservices Are an Org-Chart Problem Wearing a Tech Costume

The architecture that scales teams, deployed to solve problems teams don't have

Contents

I once watched a five-person team spend two sprints building a message queue between two services that could have been one function call in one process. Nobody could explain why, beyond “that’s how it’s done now.” It’s how it’s done at companies with a genuinely different problem than that team had, and the mismatch between the architecture and the problem is where a huge amount of avoidable homelab and small-team complexity comes from. The pattern repeats constantly: a talk from a company running thousands of engineers gets watched by a team of five, the architecture gets copied, and the actual problem that architecture was solving never gets asked about at all.

The Problem Microservices Were Actually Built to Solve

Advertisement

Microservices exist to solve a coordination problem between people, not a technical problem between machines. When a single codebase is worked on by hundreds of engineers across dozens of teams, every team touching the same shared codebase has to coordinate releases, avoid stepping on each other’s changes, and wait on the same build and deploy pipeline as everyone else. At that scale, splitting the system into independently deployable services — each owned by one team, each with its own release schedule, each communicating with the others over a defined network interface — solves a real and painful problem: it lets one team ship on Tuesday without needing forty other teams to also be ready to ship on Tuesday.

That’s the actual value proposition, and it’s a genuine one at the scale it was designed for. Conway’s Law describes the mechanism precisely: any system’s architecture ends up mirroring the communication structure of the organisation that built it. Microservices are, more than anything else, an org chart, encoded into network boundaries so that the org chart’s independence is enforced by infrastructure rather than by policy alone. If your organisation has that many independently moving teams, an architecture that lets them move independently is solving your actual problem.

What Happens When You Adopt the Architecture Without the Org Chart

A five-person team, or a solo homelabber, doesn’t have that coordination problem. There’s no release-schedule conflict to manage between yourself and yourself, no separate team whose changes you need isolating from your own. When that team adopts microservices anyway — because the tutorials, the job postings, and the conference talks all assume it — they don’t get the actual benefit the architecture provides. They get all of its cost with none of its payoff.

The cost is specific and enumerable. Every function call that used to be an in-process call within a single codebase becomes a network call between processes, with all of a network call’s new failure modes: timeouts, partial failures, retries, and the need for each service to handle the other being briefly or persistently unavailable. Every piece of shared logic that used to live in one place now either gets duplicated across services or extracted into a shared library that itself needs versioning and coordinating, quietly reintroducing the exact coordination overhead the split was supposed to remove, just at a smaller scale. Debugging a request that used to be a single stack trace becomes tracing a request across several services’ logs, ideally with distributed tracing tooling that itself needs deploying, configuring, and maintaining. None of this is optional overhead you can skip while still calling it microservices — it’s the direct, structural cost of the network boundary you introduced.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# What five services actually cost you at small scale:
services:
  auth-service:      # its own deploy, its own health check, its own logs
    build: ./auth
  users-service:     # a network call for what used to be users.get(id)
    build: ./users
  billing-service:   # depends on auth AND users being up and reachable
    build: ./billing
  notifications:     # depends on billing, retries on failure, its own queue
    build: ./notifications
  gateway:            # routes between all four, another thing to keep alive
    build: ./gateway

Five services, five deployment surfaces, five sets of logs to correlate, and a dependency graph to keep straight in your head — replacing what a single well-organised codebase with five internal modules would have done with in-process function calls and one deploy.

The Team Size Where the Trade Genuinely Flips

Advertisement

There’s a real threshold where this calculus reverses, and it’s worth naming honestly rather than pretending the architecture is simply wrong at every scale. Once a single codebase has enough contributors that a shared build genuinely blocks people meaningfully often — enough teams that “wait for the big release train” is a real, recurring cost measured in days of lost velocity rather than a hypothetical — the coordination benefit of independent deployability starts to outweigh the network-call and operational overhead microservices introduce. That threshold tends to sit somewhere in the range of many dozens to low hundreds of engineers working on the same product, not five, not fifteen, and often not fifty either. Below that threshold, a well-organised single codebase (deployed as one unit, split cleanly into internal modules with clear boundaries) gives you almost all the organisational clarity people reach for microservices to get, without paying for a single network hop.

A useful sanity check is to ask, specifically, who would be blocked from shipping this week if the system stayed a single deployable unit. If the honest answer is “nobody, because it’s the same one or two people touching all of it anyway,” the coordination problem microservices solve simply doesn’t exist yet in your organisation, regardless of how large the codebase itself has grown. Codebase size and team-coordination size are different axes, and it’s specifically the second one that determines whether the trade-off flips, not the first.

What Actually Solves the Small-Team Version of the Problem

The thing a small team is usually actually chasing when they reach for microservices is clean separation of concerns — not wanting one part of the system to become an unmanageable tangle that’s hard to reason about or change safely. That’s a real and legitimate goal. It’s just solvable with module boundaries inside a single deployable unit, which gives you the same clarity of “this part does this, that part does that” without any of the network overhead. A monolith with well-defined internal packages, clear interfaces between them, and disciplined code review is a genuinely underrated architecture for exactly this reason: it captures almost all the organisational benefit people associate with microservices while keeping a function call a function call.

For a small self-hosted stack specifically, a single well-structured application, or a small number of services split along genuinely independent operational boundaries — the web app versus the background job runner versus the database, say — usually captures the real, deployable-unit benefits (restart one without restarting everything, scale one independently if it’s the bottleneck) without splintering into a dozen tiny services each carrying network overhead for no organisational reason, because there’s no separate team on the other end of that network call to isolate from in the first place. Notice that this is a genuinely different justification for splitting anything out at all: it’s about operational independence (this piece needs to restart, scale, or fail on its own schedule) rather than team independence, and it’s a much narrower, much more defensible reason to introduce a second deployable unit than “microservices are how modern systems are architected.”

The Data Problem Nobody Mentions Until It Bites

Splitting a system into services doesn’t just add network calls between pieces of logic — it splits the data those pieces of logic operate on, and that’s where the deepest, least visible cost usually surfaces. In a single codebase with one database, an operation touching multiple entities — placing an order, say, which needs to check stock, charge a payment method, and record the order — can be wrapped in a single database transaction that either fully succeeds or fully rolls back, with the database itself guaranteeing there’s no in-between state visible to anyone.

Split that same operation across an inventory service, a payment service, and an orders service, each with its own database, and that guarantee is gone. There is no single transaction spanning three separate databases owned by three separate services; instead you’re building your own consistency logic on top of the network calls between them — deciding what happens if the payment succeeds but the inventory update fails, whether to retry, whether to reverse the payment, how to detect and clean up a request that failed halfway through. This is a genuinely solvable problem — patterns like the saga pattern exist specifically to manage it — but it’s also a problem that simply didn’t exist before the split, invented entirely by the architectural choice to give each piece of logic its own database. Teams reaching for microservices without the organisational reason for the split are frequently surprised to discover they’ve taken on this category of problem for the first time, with no corresponding benefit to show for it.

Troubleshooting: Signs You’ve Over-Split

A few honest questions catch most premature microservice adoption. If most of your services always deploy together, in lockstep, because a change to one always requires a coordinated change to another — that’s the strongest signal available that the split doesn’t reflect any real independence, and you’re carrying network overhead for a boundary that doesn’t actually decouple anything.

If debugging a typical request involves opening logs for three or more services just to follow one user action from start to finish, and there’s no separate team boundary that split reflects, the operational cost of that fragmentation is very likely exceeding whatever clarity benefit the split was meant to provide.

If your on-call rotation is one or two people covering a dozen or more services, you’ve taken on an architecture designed to let many independent teams operate autonomously and handed its full operational overhead to a team too small to actually benefit from that autonomy.

If you find yourself building shared libraries just to avoid duplicating logic across services, and those libraries need their own versioning and release process, look honestly at whether you’ve recreated the exact coordination problem the split was meant to remove, just one layer further down and with extra steps.

A further check worth running: count how many of your services have exactly one consumer, meaning exactly one other service ever calls them. A service with a single caller is a function call that got promoted to a network boundary for no operational reason, providing genuine independence to no one, and collapsing it back into whichever service calls it removes overhead without losing anything a second consumer would have benefited from, because there isn’t one.

Is the Architecture Worth Adopting?

Almost always, only once your actual organisational problem matches the one it was built to solve, and rarely before. If you’re a large organisation with genuinely independent teams that need to ship without waiting on each other, microservices are solving a real coordination cost and the network overhead is a fair trade for that independence. If you’re a small team, a startup before genuine scale, or a homelab, the honest move is to build a clean, modular, well-organised single deployable unit and resist the pull of an architecture whose entire value proposition depends on an org chart you don’t have. You can always split out a genuine bottleneck later, once you can point to an actual, measured reason — a specific component that needs independent scaling, or a specific team that genuinely needs to stop waiting on another team’s release train — rather than a tutorial’s assumption about how “proper” systems are built. Architecture decisions made to look impressive in a portfolio and architecture decisions made to solve a problem you actually have are rarely the same decision, and it’s worth being honest with yourself about which one is driving the choice in front of you. For the practical version of not reaching for orchestration prematurely, Kubernetes is a very expensive while-true loop makes the identical argument one layer down the stack, and systemd without fear: writing your first service unit is the boring, durable alternative most small teams and homelabs actually need instead.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.