service decomposition is the act of taking what was a single deployable unit and breaking it into multiple services that communicate over a network. it solves specific problems. it creates specific new ones. understanding both sides determines whether the split is worth making.
the business case for service decomposition is almost always one of three things:
independent deployability. teams that need to ship independently without coordinating with every other team that touches the codebase. a change to the billing service should not require coordinating deployment with the search service.
independent scaling. components with genuinely different resource requirements. a video transcoding service needs GPU instances; the user profile service needs none. coarse-grained scaling wastes resources when requirements diverge sharply.
organizational clarity. at a certain team size, ownership gets ambiguous in a shared codebase. a service boundary forces a clear ownership conversation: this team builds this service, owns its API contract, and is responsible for its operation.
these are legitimate reasons. "we want to use microservices" is not a reason, it is a description of the outcome.
the benefits of decomposition are real:
- teams can move independently. shipping a new feature in service A does not require touching service B's code, coordinating its tests, or waiting for its deployment pipeline.
- each service can be operated with the resources it needs. a service that handles CPU-heavy work can be scaled on compute-optimized instances; a memory-heavy cache service can be scaled on memory-optimized instances.
- a failing service (if properly isolated) does not take down the whole system. the blast radius is bounded.
- each service can choose its own data store. the one that needs full-text search uses Elasticsearch. the one that needs ACID transactions uses PostgreSQL. fit the tool to the problem.
none of this is free:
network latency and failures. every service-to-service call crosses the network. it is slower than a function call, can fail, can be slow, and requires retry logic, timeouts, and circuit breakers to handle correctly. the distributed systems failure modes from the previous chapter now apply to every inter-service call.
distributed transactions. if your monolith relied on a database transaction to ensure consistency across multiple operations, that transaction no longer works across service boundaries. you need to choose between 2PC (two-phase commit, complex and slow), saga patterns (compensating transactions, complex and error-prone), or redesigning the operation to not need cross-service atomicity.
operational complexity. every new service is a new deployment pipeline, a new monitoring configuration, a new on-call rotation scope. you need service discovery (how does service A find service B?), load balancing, distributed tracing, and centralized logging. the overhead per service is real.
testing complexity. integration testing now requires multiple running services. local development requires either running all dependencies or mocking them. contract testing (ensuring the API you consume matches what the producer provides) requires tooling that a monolith does not.
data ownership gets hard. if service A and service B both need user data, where does it live? each service owning its own data model (the microservices ideal) means synchronization is needed. a shared database violates service independence. this tension does not resolve cleanly.
the "strangler fig" pattern is useful: instead of rewriting the monolith, incrementally extract services at the seams. identify a piece of functionality with clear boundaries, an API that can be defined, and a team that will own it. extract it. leave the rest.
characteristics of a good service boundary:
- high cohesion within the service: everything inside the service is closely related and changes together.
- low coupling at the boundary: the interface between this service and others is minimal and stable.
- clear ownership: one team owns the service end-to-end.
- independent failure: the service can fail without taking down the rest of the system, and the rest of the system can tolerate its failure.
bad service boundaries:
- services that must be deployed together (deployment coupling survived the extraction)
- services that share a database table (data coupling)
- services where a change in A always requires a change in B (interface coupling)
- chatty services that make dozens of calls to each other per request (network overhead overwhelms any benefit)
the operational investment in a distributed system, tooling, observability, deployment infrastructure, happens immediately. the benefit (independent team velocity) accrues over time as the teams grow and the feature surface expands.
for a small team with a limited codebase, the overhead of maintaining a distributed system often exceeds the benefit. for a large team with a sprawling codebase, the coordination overhead of a monolith often exceeds the operational overhead of services.
this is why starting with a monolith and decomposing at the seams is often the right answer: you defer the operational investment until you can see the specific problems that decomposition solves, then solve those specific problems.