Separate business authority from physical copies so services can evolve without silently competing to write the same invariant.

Aggregate Boundaries, Bounded Contexts, and Data Ownership Across Services

A distributed architecture becomes fragile when every service can update every table or document. This lesson defines aggregate and bounded-context concepts from first principles, assigns authoritative writers, and shows why a shared database without ownership rules creates hidden coupling even when the schema itself looks convenient.

Intermediate–Advanced100–130 minutesMechanism-first modeling labPython 3.13+ · standard libraryVendor-neutral · free/local mandatory pathLast reviewed: August 2026

Learning outcomes

A distributed architecture becomes fragile when every service can update every table or document. This lesson defines aggregate and bounded-context concepts from first principles, assigns authoritative writers, and shows why a shared database without ownership rules creates hidden coupling even when the schema itself looks convenient.

01

Define aggregate, invariant boundary, bounded context, authority, copy, and integration event without assuming prior DDD knowledge.

02

Assign one authoritative writer/system for AtlasMart business facts and distinguish ownership from cached or projected copies.

03

Diagnose how shared-database write access creates semantic coupling and conflicting updates.

04

Design owner-mediated commands, events, versioned copies, tenant boundaries, and migration paths.

Implementation snapshot

Mandatory work uses Python 3.13+ standard library only in one local process. No MongoDB, Cassandra, Redis, PostgreSQL server, cloud service, Docker image, paid feature, network manipulation, or destructive failure injection is required. Optional implementation references are PostgreSQL 18, MongoDB 8.3.8 (current released patch in the 8.3 stable series as of August 29, 2026), Apache Cassandra 5.0.9, and Redis Open Source 8.10. Product-specific transaction, indexing, partition, quota, security, and licensing behavior is illustrative rather than universal.

1. Aggregate means “change together to protect an invariant”

An aggregate is a consistency boundary around state that must be changed together to preserve a business invariant. It is not simply “a large document.” AtlasMart’s order aggregate can own order lines, totals, and lifecycle state because checkout rules often need those facts together. A product catalog entry has a different lifecycle. Payment authorization has another. If one command requires a distributed transaction across many unrelated aggregates, first ask whether the invariant is truly cross-boundary or whether the workflow can use reservation/compensation.

2. Bounded context means one meaning and one owner

A bounded context is a semantic boundary within which terms and rules have one consistent meaning. “Status” in Orders might mean placed, paid, shipped, cancelled. “Status” in Payments might mean pending, authorized, captured, refunded. Sharing a column named status does not merge those meanings safely. Assign one authoritative writer to each business fact. Other services can keep copies, but copies must record source/version and must not masquerade as ownership.

Fact Authoritative context Other contexts may keep Write rule
Order lifecycle Orders Fulfillment/support projection Only Orders accepts commands that change order lifecycle
Payment lifecycle Payments Orders payment summary Only Payments changes capture/refund facts
Product title/price Catalog Search/recommendation projections Catalog owns mutable merchandising facts
Customer email Customer Notification/support copies Customer context owns validation and changes

3. AtlasMart lab: expose shared-database ambiguity

The lab creates two snapshots of the same shared row. Orders cancels an order while Payments independently writes order_status=paid. Last writer wins, but neither service has a valid semantic basis to overwrite the other.

python · AtlasMart deterministic simulation
from copy import deepcopy

owners = {
    "order.status": "Orders",
    "payment.status": "Payments",
    "product.title": "Catalog",
    "customer.email": "Customer",
}

shared_row = {"order_id":"o-100","order_status":"placed","payment_status":"pending","version":1}

# Broken shared-database ownership: two services mutate the same business fact.
orders_snapshot = deepcopy(shared_row)
payments_snapshot = deepcopy(shared_row)
orders_snapshot["order_status"] = "cancelled"
payments_snapshot["order_status"] = "paid"  # Payment service infers an Orders-owned fact.
shared_row.update(orders_snapshot)
shared_row.update(payments_snapshot)
print("broken shared row order_status:", shared_row["order_status"])
print("owner registry says:", owners["order.status"])

# Repair: owner publishes facts; other contexts keep labeled copies/projections.
orders_authority = {"o-100": {"status":"cancelled","version":2}}
payments_authority = {"pay-100": {"order_id":"o-100","status":"captured","version":4}}
fulfillment_copy = {"o-100": {"order_status":"placed","source_version":1}}

event = {"type":"OrderStatusChanged","order_id":"o-100","status":"cancelled","version":2,"owner":"Orders"}
if event["owner"] == owners["order.status"] and event["version"] > fulfillment_copy[event["order_id"]]["source_version"]:
    fulfillment_copy[event["order_id"]] = {"order_status":event["status"],"source_version":event["version"]}

print("orders authority:", orders_authority["o-100"])
print("payments authority:", payments_authority["pay-100"])
print("fulfillment copy after event:", fulfillment_copy["o-100"])
print("copy is ownership:", False)
print("cross-context correction path: command owner or reconcile from owner events")
Expected evidence

The broken shared row ends as paid even though the owner registry says Orders owns order.status. The repaired design preserves separate Orders and Payments authorities, then updates Fulfillment through an Orders-owned versioned event. The projection is a copy, not a second writer.

4. Ownership is also a security boundary

Database credentials should reinforce the semantic design. If Payments only needs an Orders projection, it should not receive unrestricted write access to Orders authority. Tenant identity must survive events and projections so a cross-context consumer cannot accidentally index one tenant’s facts under another tenant. Audit logs should record the acting service identity, command/request identifier, owner version, and resulting event so operators can reconstruct who changed authoritative state.

5. Integration without pretending boundaries remove coordination

Bounded contexts reduce accidental coordination; they do not eliminate legitimate cross-service workflows. Checkout still connects orders, inventory, and payments. The design question becomes explicit: which service owns each decision, what is reserved, which messages are idempotent, what compensation exists, and which temporary states are acceptable. Chapter 16 will explore those transaction/workflow choices directly.

6. Production judgment and bridge

Prefer clear ownership when teams deploy independently, facts have different lifecycle/invariants, or security boundaries differ. Avoid splitting an aggregate merely to match an org chart if every write then requires synchronous cross-service coordination. Plan migrations with shadow copies, versioned contracts, owner cutover, rollback, and reconciliation. The next lesson shows how derived read models can cross these ownership boundaries safely when they remain rebuildable copies.

Wrong approach: “shared database means shared ownership”

A shared physical database can be operationally convenient, but unrestricted multi-service writes create semantic ambiguity. The repair is not necessarily “one database per service”; it is explicit ownership, least-privilege writes, owner-mediated commands/events, source versions, and a migration path that preserves invariants.

Verification, cleanup, and production checklist

Verification is the deterministic program output plus the reasoning checks below. Cleanup is simply deleting the local lesson3.py file because the lab creates no external service or persistent database. In production, repeat the design with representative cardinality/skew, tail-latency, failure injection, tenant-isolation tests, restore/rebuild drills, migration rollback, and cost/capacity evidence before committing to a physical model.

Check your understanding

  1. What makes something an aggregate boundary?
  2. What is a bounded context?
  3. Can a projection be stored in another service?
  4. Why is unrestricted shared-database write access risky?
  5. Do bounded contexts remove distributed transactions?
Review the answers

1. Facts that must change together to preserve an invariant under the chosen transaction/consistency model.

2. A semantic boundary in which domain terms and rules have one consistent meaning and ownership model.

3. Yes, if it is clearly a copy with a source, version/freshness contract, authorization rules, and repair path.

4. Services can overwrite facts they do not semantically own, creating hidden coupling and last-writer-wins behavior with no business authority.

5. No. They make cross-boundary workflows explicit so coordination, reservation, compensation, and failure handling can be designed deliberately.

References

Foundational statements are kept vendor-neutral. Version-sensitive examples use current official documentation and are labeled as examples rather than definitions.

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.