Skip to main content

Architecture-to-Context Alignment: Services per Bounded Context

What This Concept Is

Bounded contexts are strategic logical boundaries. A deployable service is a physical unit. Architecture-to-context alignment is the set of decisions that map bounded contexts to deployable units, teams, data stores, and repositories.

The most defensible default is:

One service per bounded context, owned by one team, with its own data store.

But the default hides several trade-offs. The real decision runs along three axes:

  1. Logical-to-physical ratio. 1 context -> 1 service? 1 context -> many services? 1 service -> many contexts?
  2. Data ownership. Separate databases per context, a shared database with schemas per context, or a shared database with an enforced access pattern?
  3. Codebase. Modular monolith, service-per-context polyrepo, service-per-context monorepo, something else?

The viable alignments

AlignmentOne sentenceWhen it's appropriate
Microservices (1 context ↔ 1 service, DB per context)Strongest isolation, highest ops costLarger teams; strong autonomy needs; contexts change at different cadences
Modular monolith (N contexts -> 1 deployable, DB per context or schema per context)Logical isolation without deploy complexitySmall/mid teams; tight cross-context feedback; early product phase
Service-per-context + shared databaseCompromise that almost never pays offRarely appropriate; often an accidental state
Service-per-context, context with internal sub-servicesA context has internal complexity worth physical splitVery high traffic in one context; heterogeneous latency needs
"Aggregator" service across contexts (a BFF / facade)One service exposes several contexts to a clientSpecific UI or integration surface only; never for business logic

The two rules that matter

  1. Never share a database across bounded contexts without a contract. Two contexts writing to the same table is a distributed monolith in disguise.
  2. Never let a deployable cross bounded-context ownership. If two teams own pieces of the same service, the service boundary is lying.

If you hold those two rules, most of the other alignment choices are reversible.

Conway's Law, used on purpose

Conway's Law says systems mirror the communication structure of the organizations that build them. The DDD-informed move is to use it deliberately:

  • choose bounded contexts by domain coherence
  • align teams to contexts (one team, one or more contexts)
  • align services to team ownership (so deploy cadence and autonomy match team cadence)

Team Topologies (Skelton & Pais) gives a vocabulary for this: stream-aligned teams own contexts; platform teams provide shared generic capabilities; enabling teams help stream teams adopt practices; complicated-subsystem teams own deep core subdomains.

Why It Matters Here

This concept is the bridge out of this module and into the rest of the architecture topic:

  • ADRs (module 5) reference this alignment.
  • API design (module 4) consumes it -- APIs form the contracts between contexts, owned per context.
  • Service boundaries in microservice modules build on these decisions.
  • Team Topologies and organizational design follow from it.

Getting alignment wrong is how you end up with 30 services deployed by 3 people, or one modular monolith that 40 engineers step on each other's toes in.

Concrete Example

Case: Parcel Shipping -- end-to-end alignment

Year 1 -- one small team, one modular monolith

Six engineers. Five bounded contexts (Pricing, Shipping, Tracking, Billing, Support). One Django app.

apps/
pricing/ # its own SQLAlchemy models in `pricing` schema
shipping/ # models in `shipping` schema
tracking/ # models in `tracking` schema
billing/ # models in `billing` schema
support/ # models in `support` schema
_shared/ # VOs, auth, logging
infra/
postgres/ # one DB, five schemas, no cross-schema joins allowed
kafka/ # local broker for domain events via outbox

Enforcement:

  • lint rule: no import from apps.<other_context> (only apps._shared)
  • DB access rule: each context's code only touches its own schema
  • outbox in every context
  • CI test run: context A boots and tests without context B's code loaded

One deployable, clean logical boundaries. Year-1 team gets contextual isolation without paying microservices ops tax.

Year 2 -- Pricing extracts first

Pricing becomes the first extracted service. Why Pricing? It has the highest rate-of-change (rules change weekly), it is a core subdomain, and its contract to Shipping is well-defined (GetRateSnapshot).

Process:

  1. Pricing module already has its own schema and repository interface.
  2. Introduce an HTTP adapter PricingGatewayHttp implementing the same port.
  3. Deploy Pricing as its own service with its own DB (migrated from the shared Postgres).
  4. Flip Shipping to use the HTTP adapter.
  5. Other contexts follow a similar pattern when they outgrow the modular monolith.

Year 3 -- current alignment

Notes:

  • Tracking is one context but two services (command service + projection service). Internal split, external contract unchanged.
  • No team shares a database. No team shares ownership of a deployable.
  • The platform team owns the Kafka cluster, the API gateway, and observability -- generic subdomains supporting all contexts.

Anti-alignment stories, for contrast

  • "Shared orders table." Checkout, fulfillment, and support all write to orders. Schema changes require three teams to coordinate. -> distributed monolith. The fix is to give orders to one context and have the others read via events or APIs.
  • "The microservice that spans two contexts." A service called orders-api owns both order creation and fulfillment tracking because "they are close." Every feature touches both halves. -> either split the service or admit it's one context.
  • "The modular monolith with no module boundaries." Code lives in apps/* folders but everyone imports everyone. -> you have a monolith, not a modular one. Lint rules and review discipline are required, or the pattern has zero value.
  • "Context with its own service but shared database with a different context." -> still a distributed monolith; the network separation is cosmetic.

Common Confusion / Misconception

"Microservices are always better than monoliths." They are a trade, not an upgrade. For small teams and early-stage products, a modular monolith ships more value per week.

"If we do DDD, we must do microservices." We must consider microservices. We might choose modular monolith for year 1 and still be doing DDD correctly.

"A service can span two bounded contexts as long as the code is in separate modules." Short-term sometimes; long-term no. Ownership and deployability drift. The rule that no deployable crosses ownership is doing a lot of work.

"We have microservices, so we have bounded contexts." A microservice is a deployment fact. Bounded-context-ness is a language and model fact. Many companies have microservices that implement one bounded context split into services, or worse, services that implement pieces of multiple contexts.

"Data ownership = permissions." It is stricter: data ownership means one context is responsible for the invariants on that data. Others read via APIs or events, not directly.

"One team ↔ one context." Ideal but not mandatory. One team can own 2-3 small supporting contexts. Two teams on one context is a red flag.

How To Use It

When aligning:

  1. Produce the current context map (concept 6).
  2. For each context, name its team, service(s), and database(s).
  3. Walk the two rules:
    • any cross-context DB sharing? -> plan to split
    • any deployable crossing teams? -> plan to split or re-own
  4. For each context, choose the alignment pattern: modular monolith module, microservice, or internal sub-service split.
  5. Capture the rationale per context in an ADR.
  6. Re-check alignment during the quarterly boundary review (concept 9).

Check Yourself

  1. Name two rules that make a logical-physical alignment honest rather than cosmetic.
  2. When is a modular monolith a better choice than a microservices alignment?
  3. Why is "shared database between services" usually considered an anti-pattern even when each service writes to different tables?

Mini Drill or Application

For Parcel Shipping (years 2-3 scenario) or Conference Ticketing:

  1. Propose the full alignment table: context, team, service(s), database(s), deployment cadence.
  2. For each context, give the pattern (modular-monolith module / microservice / split) and a 1-2-sentence rationale.
  3. For one context, write the ADR sketch that would justify the chosen alignment -- include: decision, status, context, consequences.
  4. Flag at least one place where you would resist splitting despite peer pressure, and explain why.

Read This Only If Stuck