Skip to content

Most distributed system failures are not caused by Kafka. Or Postgres. Or consistency models.

They are caused by one design mistake:

Poorly defined aggregate boundaries.

If you get aggregate boundaries wrong:

  • Idempotency breaks.
  • Exactly-once business effects become impossible.
  • Replay becomes dangerous.
  • Scaling becomes painful.
  • Consistency becomes confusing.
  • Incidents become hard to reason about.

If you get them right:

  • Invariants are enforceable.
  • Partitioning is obvious.
  • State machines are deterministic.
  • Strong consistency is local and safe.
  • Event-driven architecture becomes manageable.

This is a deep, production-first guide to designing aggregate boundaries correctly.

1. What Is an Aggregate (In Practice)?

Forget textbook definitions.

In production systems:

An aggregate is the smallest boundary within which invariants must hold synchronously.

It defines:

  • Transaction boundary
  • Locking boundary
  • Consistency boundary
  • Partition key
  • Idempotency scope

It is not:

  • A table
  • A service
  • A microservice
  • A REST resource

It is a business invariant container.

2. Start With Invariants, Not Entities

Example: Verification system

Invariants:

  1. A verification has exactly one terminal outcome.
  2. APPROVED and REJECTED are mutually exclusive.
  3. LOCKED is irreversible.

Those invariants apply to one verification ID.

That’s your aggregate.

The aggregate boundary is:

verification_id

Not:

  • User
  • Provider
  • Organization
  • Account

Only what the invariant demands.

3. The Aggregate Determines Your Consistency Model

Inside aggregate:

  • Strong consistency
  • DB transaction
  • Row-level lock
  • Version increment

Outside aggregate:

  • Eventual consistency
  • Async events
  • Idempotent consumers

This is the golden rule:

Strong consistency inside. Eventual consistency outside.

Inside Aggregate

Safe. Deterministic. Atomic.

Across Aggregates

No cross-aggregate transactions, Ever.

4. A Common Mistake: Aggregates That Are Too Large

Consider an e-commerce system.

Naive boundary:

User aggregate

User contains:

  • Profile
  • Orders
  • Payments
  • Addresses
  • Notifications

That’s wrong.

Why?

Because these invariants are unrelated:

  • An order must not be double-shipped.
  • A payment must not be double-charged.
  • Address updates must not block orders.

If all of that lives under one aggregate:

  • Lock contention explodes.
  • Partitioning becomes impossible.
  • Scaling collapses.

Correct Decomposition

Each aggregate protects only its own invariants.

5. Heuristics for Finding Aggregate Boundaries

These are production-tested heuristics.

Heuristic 1: What Must Change Together Atomically?

If two pieces of data must be updated in the same transaction to preserve invariants they belong in the same aggregate.

Example:

  • Verification state
  • Verification version
  • Audit log entry

These change together. Same aggregate.

Heuristic 2: What Cannot Be Concurrently Modified?

If concurrent modifications would violate invariants, they must share an aggregate.

Example:

Inventory per SKU:

Invariant:

Stock cannot go negative.

That invariant applies per SKU.

So aggregate = SKU.

Do not aggregate entire catalog.

Only per SKU.

Heuristic 3: What Is Your Kafka Partition Key?

Your Kafka key should be your aggregate ID.

If you cannot answer:

“What is the partition key?”

You likely have unclear aggregate boundaries.

Ordering matters only within aggregate.

Never globally.

Heuristic 4: What Can Be Replayed Independently?

If you replay events for entity X, and it affects entity Y in dangerous ways, your boundary is wrong.

Replays must be safe per aggregate.

6. Aggregates and Deterministic State Machines

An aggregate should always have:

  • Explicit state machine
  • Guarded transitions
  • Version field (optional but recommended)

Example: Order

Illegal transitions:

  • SHIPPED → CREATED
  • DELIVERED → PAID

Guard transitions inside transaction.

7. Aggregates and Idempotency

Idempotency keys must map to aggregate.

If you cannot scope idempotency to an aggregate ID, you will eventually double-apply effects.

Correct pattern:

processed_events (event_id, aggregate_id)

Dedup inside aggregate boundary.

Never global unless absolutely necessary.

8. Aggregates and Scaling

Bad boundary:

Organization aggregate

Every action:

  • Create user
  • Create verification
  • Update settings

All require locking organization row. Hot partition. Throughput collapse.

Correct pattern:

Each entity with independent invariants becomes its own aggregate.

9. Over-Splitting Is Also Dangerous

Too small aggregates create cross-aggregate invariants.

Example:

You split:

  • OrderHeader
  • OrderItems
  • OrderPayment

But invariant:

Order cannot ship if items not reserved and payment not confirmed.

Now you need distributed coordination.

Better:

Order is single aggregate. OrderItems belong inside it.

10. Case Study: Designing Verification at Scale

We define:

Aggregate = verification_id

What does it contain?

  • State
  • Version
  • Provider reference
  • Decision metadata

What does it NOT contain?

  • User profile
  • Organization limits
  • Risk scoring engine

Those are separate aggregates.

Verification Aggregate Architecture

Single transaction per state transition.

Risk Enrichment as Separate Aggregate

Risk recalculation does not block verification transaction. Loose coupling.

11. Aggregates and Consistency Tradeoffs

Aggregate boundaries define:

  • Where strong consistency is required.
  • Where eventual consistency is acceptable.

If your system requires strong consistency across multiple aggregates:

You probably designed them incorrectly. Or your invariant is global and needs a different strategy.

12. Anti-Patterns

❌ Aggregate = Service

Service boundaries are deployment concerns.

Aggregates are consistency concerns.

Not the same.

❌ Aggregate = Database

Multiple aggregates can live in same database.

Single aggregate can span multiple tables.

❌ Global Locks for Cross-Aggregate Invariants

This destroys scalability.

Instead:

  • Redesign invariant
  • Introduce compensation
  • Or introduce new aggregate representing invariant

13. Designing Boundaries Step-by-Step

  1. List business invariants.
  2. Group invariants by entity.
  3. Ask: must this be enforced synchronously?
  4. Define smallest boundary that enforces it.
  5. Assign aggregate ID.
  6. Use aggregate ID as:
    • DB primary key
    • Kafka key
    • Idempotency scope
    • Version scope

If one invariant spans two aggregates, reconsider.

14. The Mental Model

Think of aggregates as:

Miniature strongly consistent islands in an eventually consistent ocean.

Strong locally. Eventual globally. That is scalable architecture.

Final Thought

Distributed systems do not fail because Kafka reorders messages.

They fail because:

  • Aggregates are oversized.
  • Invariants are unclear.
  • Boundaries are blurry.
  • Consistency expectations are mismatched.

Designing aggregate boundaries is not academic.

It is the foundation of:

  • Idempotency
  • Exactly-once business effects
  • Replay safety
  • Scalable partitioning
  • Operational sanity

Get the boundary right and the rest of the system becomes tractable.

Get it wrong and no amount of infrastructure will save you.

Need this level of clarity for your own software system?
Subera helps companies plan, build, integrate, modernize, and maintain dependable software platforms.
Where Subera can help

Typical work around topics like this

Technical direction
Clarify architecture, risks, tradeoffs, and delivery priorities.
Software delivery
Build systems, workflows, integrations, APIs, and platforms.
Platform evolution
Improve reliability, maintainability, observability, and scale.