Orientation
Component decomposition is the practice of breaking a software system into distinct, interchangeable parts that can be developed, deployed, and evolved independently. It ceases to be a theoretical exercise and becomes a daily operational necessity when a codebase or infrastructure footprint grows beyond a single team’s ability to reason about it whole, when scaling demands that individual workloads receive independent resource quotas, or when organizational boundaries require parallel ownership of different functional domains. This reference page is organized around the tasks you are most likely performing: carving out a new boundary, defining its interface, integrating it with existing pieces, and diagnosing the failures that arise when decomposition diverges from the system’s actual runtime behavior. Every command, flag, and configuration key shown below belongs to shipping, widely deployed tools; no invented CLI or vendor‑specific internal detail is included.
Establishing a new component boundary
When you need to carve a new standalone unit from an existing system, the first step is a clear directory and module structure that enforces separation without requiring a full rewrite. In a typical container‑oriented workflow this looks like creating a dedicated build context, a minimal Dockerfile, and a docker-compose.yml (or equivalent orchestration manifest) that references only the artifacts this component requires.
mkdir -p services/auth-service
cd services/auth-service
touch Dockerfile docker-compose.yml main.py
# Dockerfile — minimal production‑ready base
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8080
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
# docker-compose.yml — isolates the component’s runtime
services:
auth:
build: .
ports: ["8080:8080"]
environment:
- DATABASE_URL=postgres://auth@db/authdb
depends_on:
- db
# Build and bring up only this component
docker compose up --build -d auth
The sharp edge here is build‑context leakage: if Dockerfile copies parent‑directory files indiscriminately, dependency versions or configuration files from other components can silently shadow the intended ones. Always pin requirements.txt and copy only the files your component explicitly needs.
Defining and validating component interfaces
Once a boundary exists, the next task is specifying and verifying the contract this component presents to the rest of the system. In practice this means an API specification, a protobuf service definition, or a simple OpenAPI document that is version‑controlled alongside the component’s source.
# Generate a skeleton OpenAPI file from a description
swagger-cli create /path/to/auth-service-api.yaml --type api
# auth-service-api.yaml — public contract, versioned at the repo level
openapi: 3.1.0
info:
title: Authentication API
version: 1.2.0
paths:
/token:
post:
summary: Issue an access token
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Credentials'
responses:
'200':
description: Token issued
content:
application/json:
schema:
$ref: '#/components/schemas/Tokens'
components:
schemas:
Credentials:
type: object
required: [username, password]
properties:
username:
type: string
password:
type: string
Tokens:
type: object
properties:
access:
type: string
refresh:
type: string
# Validate the spec against a running deployment
curl -sSf http://localhost:8080/.well-known/openapi | jq .
A common failure mode is drift between the spec and the implementation: a developer adds a field to the database model but forgets to update the OpenAPI definition. The first sign is a 422 Unprocessable Entity response from the API gateway when a client sends the newly added payload. The fix is to treat the specification as the source of truth and generate server stubs from it, rather than hand‑coding endpoints and retrofitting the spec.
Refactoring a monolith into decomposed components
Organizations frequently start with a monolithic codebase and incrementally extract subsystems into separate services. A practical, low‑risk path uses git subtree to split a directory into its own repository while preserving history, then configures it as a Docker‑compose service or a Kubernetes manifest.
# Split the billing directory into a new repo while keeping full history
git subtree split --prefix billing -o billing-branch
git checkout billing-branch
git repack -a -d # prune old objects, keep only the split history
# Initialize a new repo for the extracted component
cd billing-branch
git init
git add .
git commit -m "Initial billing component extract"
# Push to a remote if desired
git remote add origin [email protected]:org/billing-service.git
git push -u origin main
# In the parent repo, replace the extracted directory with a docker‑compose reference
cat > docker-compose.yml <<'EOF'
services:
billing:
image: ghcr.io/org/billing-service:main
environment:
- DATABASE_URL=postgres://billing@db/billingdb
depends_on:
- db
EOF
docker compose up -d billing
The sharp edge in this workflow is import‑cycle breakage: if the billing module still imports from other parts of the monolith that were not extracted, the new service will fail at startup with an ImportError (Python) or Cannot find module (Node.js). The only cure is a systematic audit of import statements and require() calls across the extracted tree, followed by replacing internal references with API calls or shared library packages published to an internal package registry.
Another silent failure is configuration drift: environment variables that were previously set in a single docker run -e line now need to be distributed across the parent compose file, the child service file, and possibly a Kubernetes ConfigMap. Missing or misspelled vars surface as connection refused or authentication failed errors only after the service has been running for hours, because the component retries connections with back‑off logic before surfacing the underlying config problem.
Deploying and versioning decomposed units
Once components are independent, deployment workflows shift from “run the whole app” to “deploy one service at a time.” In a Kubernetes environment this is typically handled with kubectl apply and strategic resource naming; in a Docker‑compose setup it’s docker compose up with selective service names.
# Apply only the auth and billing manifests to a cluster
kubectl apply -f k8s/auth-deployment.yaml -f k8s/billing-service.yaml
# Rolling update of the auth service with zero‑downtime
kubectl set image deployment/auth-service auth-service=ghcr.io/org/auth-service:v1.3.0
kubectl rollout status deployment/auth-service --timeout=120s
# Canary release of a new billing version using a label selector
kubectl patch deployment billing-service -p '{"spec":{"template":{"spec":{"containers":[{"name":"billing","image":"ghcr.io/org/billing-service:v2.1.0"}]}}}}'
kubectl get pods -l app=billing -o jsonpath='{.items[*].status.containerStatuses[0].ready}'
A frequent production blind spot is version skew across dependent components: if the auth service is upgraded to v1.3.0 but the billing service still references v1.2.0, the API contract defined in the OpenAPI spec may no longer match, causing 502 Bad Gateway responses from the ingress controller. The only reliable guardrail is a CI pipeline that validates the merged OpenAPI document against the running service’s /schema endpoint before promoting either component.
Diagnosing decomposition‑related failures
When a decomposed system misbehaves, the failure is rarely in a single component; it is usually in the interactions between them. Systematic diagnosis begins with narrow‑scope logging and ends with end‑to‑end request tracing.
# Stream logs for a single pod without flooding the terminal
kubectl logs -f deployment/auth-service --tail=20
# Query the database directly to check for stale state that a component may be holding
psql -h db -U admin -d authdb -c "SELECT * FROM sessions WHERE expires_at < now() - interval '1 hour';"
# Verify that the expected port is actually listening inside a Docker container
docker inspect --format='{{(index .NetworkSettings.Ports "8080/tcp")}}' auth
Silent failure patterns that catch teams off‑guard include:
- Port collisions on the host: two decomposed services configured to bind to
0.0.0.0:8080will both start, but the firstdocker runto claim the port will starve the second of inbound traffic. The second service logs no error; it simply receives no requests. - Environment‑variable shadowing: when a
docker-compose.ymldefinesENV_VAR=valueand the parentDockerfilealso containsENV ENV_VAR default, the compose‑defined value wins, but only if the compose file is processed after theDockerfileENVinstruction. Reverse ordering silently keeps the default. - Kubernetes service mesh mutual‑TLS misconfiguration: if two services expect mutual TLS but one is annotated with
sidecar.istio.io/inject: "false"while the other has it enabled, TLS handshakes fail withgrpc: Server.Serve failed: unable to parse peer certificate. The error appears as aconnection reset by peerwith no TLS‑related logs, because the kernel‑level connection succeeds but the application‑level handshake does not.
Recovering from these typically requires narrowing the scope: disable one variable at a time, use strace or tcpdump to observe actual network traffic, and always validate configuration inheritance chains before promoting a change.
Quick checklist for healthy component decomposition
- [ ] Boundary clarity: each component’s directory, Dockerfile, and manifest reference only its own artifacts.
- [ ] Contract first: OpenAPI/protobuf spec exists, is version‑controlled, and is validated against the running service before merge.
- [ ] Import audit: all internal
import/requirestatements cross‑component boundaries have been replaced with API calls or published packages. - [ ] Config isolation: environment variables, config maps, and secrets are defined per‑component with no overlap or shadowing.
- [ ] Version gate: CI checks that a component’s published image tag matches the spec version referenced by dependent services.
- [ ] Traceability: every inbound request can be traced through component boundaries using a consistent request‑ID header propagated by the ingress layer.
Component decomposition ceases to be a one‑time architecture decision and becomes a repeatable practice when these checks are automated and enforced as part of every change, not as a post‑hoc audit. The tools and commands above are concrete starting points; map them to the specific platforms your organization runs, and adjust the flags and keys to match your actual runtime environment.
This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
Join the Newsletter
Get the latest updates and deep insights shipped directly to your inbox.