17.2.1.5 Service Separation
A focused guide to Service Separation, connecting core concepts with practical Docker and container operations.
Service separation is the architectural decision of where to draw boundaries between genuinely distinct services within a containerized system, a higher-level concern than the one-process-per-container convention, focused on deployment independence, failure isolation, and ownership clarity at the level of an entire service rather than at the level of a single container's internal process structure.
Deployment independence as the primary driver
The strongest practical reason to separate two pieces of functionality into distinct services is the ability to deploy, version, and release each independently of the other, without one's release schedule or rollout risk being tied to the other's:
docker build -t my-api:1.5.0 ./api
docker build -t my-worker:2.3.0 ./worker
docker compose up -d --no-deps api
If updating the API never requires redeploying the worker, and vice versa, the two genuinely warrant separation into distinct services; if they always need to change and deploy together in lockstep, that is a signal they may actually be more tightly coupled than the separation suggests, and the boundary deserves reconsideration.
Failure isolation between services
Separating services means a failure or resource exhaustion in one does not directly take down the other, which is a meaningful operational benefit when the two genuinely have different risk profiles or failure modes:
services:
api:
deploy:
resources:
limits:
memory: 512M
report-generator:
deploy:
resources:
limits:
memory: 4G
A memory-intensive report generation service crashing under load has no direct effect on the separately deployed, separately resourced API service, whereas the same two responsibilities bundled into one service would mean a crash in either affects both simultaneously.
Ownership and team boundaries
In organizations with multiple teams, service boundaries often, and reasonably, align with team ownership boundaries, letting each team independently build, deploy, and operate the services they own without needing close coordination with other teams for every change:
team-payments/ → owns: payment-service, payment-worker
team-catalog/ → owns: catalog-service, search-indexer
This alignment is not a hard architectural requirement, but it is a common, practical pattern, and service boundaries that cut awkwardly across team ownership lines often produce exactly the kind of coordination overhead that separation is meant to avoid in the first place.
Technology heterogeneity across services
Separate services can be built using entirely different languages, frameworks, and runtime requirements without any conflict, since each is packaged into its own independent image with its own independent dependencies:
FROM node:20-alpine
FROM golang:1.22-alpine
A team choosing to rewrite one specific service in a different language for performance reasons can do so without any impact on the other services in the system, provided the interface between them, an API contract, a message queue format, remains stable and compatible.
Avoiding premature, excessive fragmentation
Service separation has real coordination and operational overhead, network calls instead of in-process function calls, additional deployment and monitoring surface area, distributed tracing complexity, and fragmenting a system into too many overly fine-grained services before there is a genuine, demonstrated need for that level of separation introduces this overhead without a corresponding benefit:
user-service, user-profile-service, user-preferences-service, user-avatar-service
If these four "services" always change together, are always deployed together, and have no actual independent scaling or failure isolation need, they are very likely better represented as a single, more cohesive service than as four separately deployed, separately operated ones.
Defining the interface between separated services deliberately
Once a genuine separation boundary is identified, the interface between the resulting services, an API contract, a message format, an event schema, needs to be deliberately designed and versioned, since this interface becomes the actual coupling point between otherwise independently deployed services:
openapi: 3.0.0
paths:
/orders/{id}:
get:
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Order'
A well-defined, versioned interface allows each side of the separation to evolve independently as long as the contract itself is respected, which is the actual mechanism that makes deployment independence genuinely achievable rather than merely assumed.
Revisiting service boundaries as a system evolves
Service boundaries drawn early in a system's life, often based on incomplete information about how the system would actually grow and be used, can become a poor fit over time, either too fragmented for the actual coordination needs that emerged, or too monolithic for the independent scaling and deployment needs that developed; periodically revisiting whether existing boundaries still make sense, rather than treating them as permanent and unquestionable, keeps the architecture aligned with actual, current needs:
git log --oneline -- api/ worker/ | head -20
Reviewing how frequently two services have actually needed to change together historically is a concrete, evidence-based way to assess whether a given boundary continues to make sense, rather than relying purely on the original, possibly outdated reasoning behind how it was first drawn.
Common mistakes
- Drawing service boundaries based on a theoretical or aspirational future need rather than an actual, demonstrated requirement for independent deployment or failure isolation.
- Fragmenting a system into too many overly fine-grained services, introducing distributed system coordination overhead without a corresponding, genuine benefit.
- Allowing service boundaries to cut across team ownership lines in a way that creates unnecessary cross-team coordination for routine changes.
- Not deliberately designing and versioning the interface between separated services, leaving the actual coupling point between them informal and prone to breaking changes.
- Treating service boundaries as permanent and unquestionable rather than periodically revisiting whether they still reflect the system's actual, current needs.
Service separation is a deliberate architectural decision driven by genuine deployment independence, failure isolation, and ownership needs, distinct from the more mechanical one-process-per-container convention, and drawing these boundaries well requires evidence of actual independent change patterns rather than aspiration, paired with a deliberately designed interface that makes the resulting independence genuinely achievable in practice.