bigonotes
System DesignArchitecture

Microservices: Boundaries, Coupling and Trade-offs

When to split a monolith, how to draw service boundaries, why each service owns its data, and what the network costs you.

7 min readLast updated

A microservice architecture builds one application as a set of small services. Each service owns one business capability, runs in its own process, and talks to the others over the network. Teams can then change, deploy and scale each part on its own, at the price of running a distributed system. This note covers how to draw good boundaries and when the trade is worth it.

Monolith vs microservices#

A monolith is one deployable unit. All features share one codebase, one process and usually one database. Calls between features are in-memory function calls.

That's not a bad thing. A monolith is simple to build, test, deploy and debug.

Problems appear as the codebase and team grow. Code for one feature spreads across many modules, so it gets hard to know where a change belongs. Every release ships everything, so one risky change delays all the others.

Microservices fix this by splitting along business boundaries: orders, payments, search, notifications. The boundary tells you where the code for a feature lives. Here's how the two styles compare:

Monolith Microservices
Deploy Whole app at once Each service alone
Scale Copy the whole app Scale only the busy service
Calls between parts In-process, fast Network, slower and can fail
Data One shared database One database per service
Debugging One stack trace Traces across many services
Best fit Small team, early product Many teams, clear domains

Many teams start with a well-structured monolith and split out services later, when a boundary is clear and a team needs to move on its own. Martin Fowler calls this "monolith first".

Small and autonomous#

Once you split, each piece needs two properties to count as a good microservice.

  • Small and focused. A service does one job well, such as "manage orders". It is small enough for one team to understand fully.
  • Autonomous. A service runs, deploys and fails on its own. It usually runs in its own container or VM. Other services reach it only through its public API, never through its internals or its database.

Autonomy is the point. If you must deploy two services together every time, they are one service split in the wrong place.

Loose coupling, high cohesion#

Autonomy depends on where you draw the lines between services. Two ideas guide that.

Loose coupling means a change in one service rarely forces a change in another. You get it from:

  • Stable, versioned APIs. Consumers depend on the contract, not the code behind it.
  • Asynchronous messages where the caller does not need an answer now.
  • Data isolation. No service reads another service's tables.

High cohesion means everything about one responsibility lives in one service. All order logic (create, update, history) belongs in the order service, not spread across three. When related logic is split, every feature change touches several services and several teams.

Bounded contexts#

Domain-driven design (DDD) gives a practical tool for finding boundaries: the bounded context. A bounded context is a part of the business where each term has one clear meaning. "Customer" in billing (payment method, invoices) differs from "customer" in shipping (address, delivery window). Each context is a good candidate for a service, and each keeps its own model of shared ideas.

Too small is a problem too#

Over-splitting creates nanoservices: services so small that they do trivial work. Every feature then needs many network hops and many deploys. You pay the full cost of distribution for little gain. Group related behavior until a service has a meaningful job.

Database per service#

Data is where autonomy most often breaks down, so each service owns its data store. Other services get that data only through the owner's API or through events it publishes.

The opposite is the shared database anti-pattern: several services read and write the same tables. Here's how the two compare:

Shared database Database per service
Consistency Easy, one transaction Harder, eventual consistency
Schema change Can break every service Affects one service
Coupling Tight, hidden in the schema Only through APIs and events
Tech choice One engine for all Best engine per service

The shared database feels easy at first. But the schema becomes a public contract that nobody can change safely. Database per service keeps services independent. It also lets each one pick a fitting store, for example a search engine for search and a relational database for payments.

The cost is that one business action may now touch several databases. You cannot wrap them in one ACID transaction. Instead you use patterns such as the saga (a chain of local transactions, each with an undo step) and accept that data is eventually consistent.

Synchronous vs asynchronous integration#

Since services can't read each other's data directly, they have to ask for it or be told about changes. They talk in two broad styles.

Synchronous request/response. The caller sends a request and waits for the answer. REST over HTTP and RPC frameworks such as gRPC work this way.

  • Good for: work that needs an answer now, such as "is this payment authorized?"
  • Cost: the caller depends on the callee being up and fast. A chain of synchronous calls is only as available as its weakest link.

Asynchronous messaging. The sender puts a message on a queue or publishes an event and moves on. See pub/sub and event-driven architecture.

  • Good for: work the user does not wait on, high-volume flows, and fan-out to many consumers.
  • Cost: harder to trace and debug. You must handle duplicates, ordering and failed messages.

Here's how the common options compare.

Style Coupling Latency to answer Failure impact
REST Medium One round trip Caller fails or waits
RPC (gRPC) Tighter, shared schema Low, binary encoding Caller fails or waits
Queue / events Loose No direct answer Messages wait, retry later

Most systems mix both. A typical rule: use synchronous calls for queries and for commands the user waits on. Use events to tell other services that something happened.

One more piece sits in front of all of this. Clients usually reach the services through an API gateway, which routes each request and handles auth and rate limits in one place.

The cost of distribution#

Splitting a process into services turns function calls into network calls. That changes a lot.

  • Latency. A network call is far slower than an in-memory call. A request that fans out to five services pays five round trips.
  • Partial failure. Any call can time out or fail. You need timeouts, retries with backoff, idempotency and circuit breakers (which stop calling a service that keeps failing). See retries and circuit breakers.
  • Data formats. Services must agree on message formats (JSON, Protocol Buffers) and evolve them without breaking each other.
  • Operations. Many services mean many pipelines, dashboards and on-call rotations. You need service discovery (a way for services to find each other's addresses), load balancing, and central logs, metrics and traces.
  • Testing. End-to-end tests span many services. Contract tests help check that each API still matches what its consumers expect.

Platforms such as Kubernetes and service meshes take over some of this work. They also add their own learning curve.

When to use microservices#

Microservices pay off when the organization, not only the code, needs to scale.

Good signs:

  • Several teams block each other in one codebase.
  • Parts of the system have very different load or reliability needs.
  • Domain boundaries are clear and stable.
  • You already have solid CI/CD, monitoring and on-call habits.

Warning signs:

  • A small team or a new product whose domain still changes weekly.
  • Services that must always deploy together.
  • No central observability yet.

Key takeaways#

  • A microservice is small, owns one business capability, and deploys on its own.
  • Draw boundaries for high cohesion and loose coupling. DDD bounded contexts are a good guide.
  • Each service owns its data. A shared database couples services through the schema.
  • Use synchronous calls when the caller needs an answer now. Use messages and events for everything else.
  • Distribution adds latency, partial failure and operational load. Start with a modular monolith unless you have a clear reason to split.
Revision notes · 2 min

Core idea#

A microservice architecture builds one application as small services. Each service owns one business capability, deploys on its own, and talks to others over the network.

Key points#

  • A monolith is one deployable unit with in-process calls and usually one database.
  • A good service is small, focused and autonomous. Others reach it only through its public API.
  • Loose coupling: a change in one service rarely forces a change in another.
  • High cohesion: all logic for one responsibility lives in one service.
  • DDD bounded contexts help you find service boundaries.
  • Too-small services (nanoservices) pay the full cost of distribution for little gain.
  • Two services that always deploy together are one service split in the wrong place.

Data ownership#

Shared database Database per service
Consistency Easy, one transaction Eventual, sagas
Schema change Can break every service Affects one service
Coupling Tight, hidden in schema Through APIs and events

Integration styles#

Style Use when Cost
Synchronous (REST, gRPC) Caller needs an answer now Caller depends on callee being up
Asynchronous (queues, events) Work the user does not wait on, fan-out Duplicates, ordering, harder tracing

Cost of distribution#

  • Network latency on every call.
  • Partial failure: you need timeouts, retries with backoff, idempotency and circuit breakers.
  • Agreed message formats that evolve safely.
  • More pipelines, dashboards and on-call; central logs, metrics and traces.

Pitfalls#

  • Shared database across services.
  • Splitting before domain boundaries are clear.
  • Long chains of synchronous calls: availability drops with each hop.
  • No central observability.

Say it in an interview#

  • "I start with a modular monolith and split a service out when a boundary and a team are clear."
  • "Each service owns its data; others get it through its API or its events."
  • "Sync for queries the user waits on, events for telling others something happened."

Found a mistake? Email ask@bigonotes.com

Further reading#