ML
Microservices

Service Boundaries: Where to Draw the Line

The hardest design question in microservices isn't 'how do they talk?'. It's 'what is the right thing to be its own service?'

April 02, 20269 min readMicroservicesArchitecture

The point of microservices is independent deployability. The cost is a distributed system. The line between "deserves to be a service" and "should be a module inside another service" is where every team's instinct fights its history.

1. The wrong reasons to split

1.1 "Each team gets a service"

Organisational design is real (Conway's Law), but services should not be one-per-engineer or one-per-squad. A service inherits a tax: deploy, monitor, on-call, schema migration, contract maintenance. Splitting too fine multiplies that tax.

1.2 "We want to use a different language"

Polyglot is a side effect, not a reason. If the data is shared, the language doesn't change the boundary.

1.3 "It will be easier to refactor later"

A monolith with clear module boundaries can be split when needed. A premature microservice with leaky boundaries cannot easily be merged — the wire protocol calcifies them.

2. The right reasons to split

2.1 Different rate of change

If module A ships twice a day and module B once a quarter, deploying them together hurts both. The fast one is constrained, the slow one is destabilised. Split.

2.2 Different scale profile

If the recommendation engine needs 20 GPUs and the user service needs 0.1 CPU, putting them in one process is wasteful. Split.

2.3 Different availability requirement

Auth has to be available when checkout is degraded. If they share a deploy, they share an outage. Split.

2.4 Different data ownership

If two pieces of code write the same table, they are the same service. If they write different tables and only read each other's via API, they are different services. The data is the boundary.

3. The "shared database" smell

The #1 way teams end up with the costs of microservices and none of the benefits: two services that both write to the same Postgres tables.

Now a schema change requires both teams to deploy in lockstep. The whole independence story is gone. The right shape:

  • Each service owns its tables. Period.
  • Other services read via the owning service's API, or via an event stream the owning service emits.
  • No cross-service joins in SQL. Ever.

When two teams need the same data, they either pick an owner (and the other becomes a consumer) or the data is genuinely shared and goes into a third "domain service."

4. Cohesion test: would I deploy these together?

Sketch the proposed service boundary. Ask: for every plausible feature in the next 6 months, does it touch only this service, or does it touch this one and another?

If 80% of features cross the boundary, you have drawn it wrong. Either move the boundary or stop pretending these are separate concerns.

5. The carving exercise

For an existing monolith, two techniques work in practice:

5.1 Event storming

Gather domain experts and engineers, post-it-note every business event ("Order placed," "Payment received"), cluster events into aggregates, cluster aggregates into bounded contexts. The contexts often map cleanly to services.

5.2 Strangler fig

Build the new service alongside the monolith. Route a small slice of traffic through it. When confidence is high, expand. When the slice is 100%, delete the monolith code path.

Resist the urge to do big-bang rewrites. They take 2× the estimate and lose features along the way.

6. The smell checklist for "should we merge these?"

  • Service A and B are deployed together every time.
  • Service A breaks → service B's tests fail.
  • A change to service A's data model forces a change to service B's API.
  • Service B's monitoring includes service A's health checks.
  • A single team owns both.

Three or more of those, and you are paying microservice costs for monolith behaviour. Merge.

The honest part

Get this wrong and you re-platform in 18 months. Get it right and you barely notice the architecture — features ship, services scale independently, on-call is bounded. The decisive variable is not REST vs gRPC, not Kafka vs RabbitMQ, not Postgres vs MongoDB. It is whether your boundaries respect your data and your release rhythm.

SharePostLinkedIn

Reader Discussion

2 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Isabella Costa· Junior EngineerKind words

    saved this. sharing at standup tomorrow — we've had exactly this problem for 2 sprints and nobody on the team had framed it this way 🙏

    Apr 04, 2026·2 days later
  2. Kenta Yamada· Tech LeadAsks

    would love a war-story follow-up. principles are clear; the actual debugging session is where the interesting stuff lives. there's a real shortage of "here's the dashboard, here's the thread we pulled, here's where we got stuck for 90 mins" content.

    Apr 06, 2026·4 days later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email