Gabriel Schenker

Gabriel Schenker

Consistency Belongs to Decisions, Not to Aggregates

Consistency Belongs to Decisions, Not to Aggregates

What if we could enforce business invariants without first deciding which aggregate owns them?

Imagine we're building a fairly simple application. Users can register, choose a username, and create an account.

There's one requirement: every username must be unique.

Nothing particularly complicated about that, right?

We might start by modelling a User aggregate. It handles registration, profile updates, perhaps account activation and deactivation. We use Event Sourcing, so every user has an event stream recording what happened to that account.

Now two people attempt to register with the same username at almost exactly the same time.

Both check whether the username is available. Both receive a positive response. Both proceed with registration.

Which one gets the username?

Our User aggregates cannot answer that question. Each only knows its own history. Neither can protect an invariant that concerns all users.

And suddenly, a seemingly trivial business requirement has become an architectural problem.

The aggregate dilemma

For years, Domain-Driven Design has taught us to use aggregates to protect business invariants.

An aggregate defines a consistency boundary. We load its current state, evaluate a command, and persist the resulting changes atomically. In event-sourced systems, this typically means reading an aggregate's event stream and appending new events with an expected stream revision.

This is a useful pattern. It gives us a predictable way to reason about concurrent modifications, state transitions, and business rules.

But what happens when the information required to make a decision doesn't fit neatly inside one aggregate?

Let's return to our username example.

We could introduce a dedicated username registry. Or use a reservation mechanism. Perhaps maintain a separate table with a unique constraint.

All perfectly legitimate solutions.

But notice what happened.

We started with a simple business rule and introduced additional architectural mechanisms because the rule didn't fit the consistency boundaries we'd already chosen.

We could argue that we chose the wrong aggregates. Maybe. But what would the right aggregate be? One enormous aggregate containing every username in the system?

Clearly not.

And this problem isn't unique to registration.

When a decision concerns multiple aggregates

Consider a university enrollment system.

Students can subscribe to courses, subject to two constraints:

- A course has a limited number of available seats. - A student may subscribe to no more than five courses.

Both rules must hold when an enrollment is accepted.

Following a conventional aggregate-based approach, we might identify a Student aggregate and a Course aggregate.

The Student knows which courses the student has subscribed to. The Course knows its capacity and current subscriptions.

Now a student wants to enroll in a course.

Where does that decision belong?

If the Student aggregate handles the command, it can enforce the student's maximum number of subscriptions, but it cannot independently guarantee that the course still has an available seat.

If the Course aggregate handles it, the opposite is true.

And checking the other aggregate before executing the command doesn't necessarily solve the problem. Between reading that information and committing the change, another operation might invalidate our assumptions.

We need to protect both invariants against concurrent changes.

Again, there are established solutions. We might redesign our aggregate boundaries, introduce coordination, use transactions spanning multiple streams, or accept some form of eventual consistency with compensation.

But we have to ask ourselves something.

Why are we spending so much effort determining which aggregate should own the decision when the business requirements already tell us exactly what information is needed?

The decision is straightforward:

Can this student enroll in this course, given the student's current subscriptions and the course's remaining capacity?

Both sets of facts matter. Neither should arbitrarily dictate the structure of the other.

Checkout makes the problem even more interesting

Now consider a typical e-commerce checkout.

A customer has added several items to a shopping cart and is ready to place an order.

Before accepting the order, we may need to establish that:

- Sufficient inventory is available for each product. - The prices presented to the customer are still valid. - A promotional voucher can still be redeemed. - The customer hasn't exceeded an applicable purchasing or credit limit.

Depending on the design, these responsibilities might be distributed across Cart, Product, Inventory, Promotion, Customer, and Order aggregates.

Each aggregate makes sense in isolation.

Yet the business decision involves information from several of them.

We could introduce an orchestrator, coordinate updates, reserve inventory ahead of time, or implement a saga.

Sometimes those are exactly the right choices.

But there is a subtle danger: we might end up introducing multiple steps, intermediate states, and compensating actions for something the business expects to be a single consistent decision.

Of course, checkout can involve genuinely independent systems. A payment provider, an external warehouse, or a shipping service cannot magically participate in one local transaction. We need to account for that.

But suppose inventory, prices, vouchers, and order commitments are all represented in the same event store.

Why should we automatically treat them as separate consistency boundaries simply because we modelled them as separate aggregates?

Why couldn't the decision itself determine which facts need to remain consistent?

What if we turn the question around?

This is where Dynamic Consistency Boundaries (DCB) become interesting.

Rather than identifying a fixed aggregate and asking it to handle a command, we start with the business decision.

We ask:

Which facts must I know to make this decision, and which changes would invalidate it before I can commit the outcome?

From there, we construct a decision model using the relevant events.

This model doesn't have to represent an entire entity or a permanent domain object. It only needs the information required for the decision at hand.

For username registration, we need to know whether the normalized username has already been claimed.

For course enrollment, we need the student's current subscriptions and the course's remaining capacity.

For checkout, we might need stock availability, valid prices, voucher eligibility, and applicable spending limits.

These are different decisions. Naturally, they require different information.

Why should we expect them all to fit into a small collection of predefined, fixed aggregate boundaries?

How DCB protects the decision

The underlying mechanism is surprisingly straightforward.

In a DCB-capable event store, events can be selected by their types and tags. We can retrieve the events relevant to a particular decision without restricting ourselves to one aggregate stream.

We then use those events to build a purpose-specific decision model.

After evaluating the business rules, we attempt to append the resulting event or events, together with a condition that protects the information on which our decision was based.

The event store atomically verifies that no relevant events matching that condition have been appended since the position at which we evaluated the decision.

If something relevant changed, the append fails. We can reload the facts, reevaluate the decision, and, where appropriate, retry.

Consider username registration again.

Both registration requests might initially find that john.doe is available.

The first successfully appends UserRegistered, tagged with the normalized username.

The second request attempts to append its event, but the event store detects that another event matching its consistency condition has appeared in the meantime.

The second append fails.

We have enforced username uniqueness without introducing a global User aggregate or a separate username reservation process.

Now return to university enrollment.

A decision model can consider both the student's subscriptions and the course's capacity. Its consistency condition covers events relevant to either constraint.

If another student takes the last available seat, or the student reaches their subscription limit while the decision is being processed, the conditional append detects the conflict.

And in our checkout example, a decision model could combine the information necessary to validate the order and atomically record the appropriate order and reservation events, provided those commitments live within the same event store.

Notice something important here.

We're still enforcing invariants. We're still protecting decisions against concurrent modifications. We're still dealing with consistency.

What has changed is how we define the boundary.

The consistency boundary follows the decision.

It no longer has to coincide with the lifetime or structure of a particular aggregate.

But aren't aggregates already about invariants?

At this point, an experienced DDD practitioner might reasonably object.

Aggregates were never supposed to be arbitrary collections of entities. Their primary purpose is precisely to protect business invariants.

That's correct.

The problem isn't that aggregates are inherently wrong. In many domains, an aggregate is a natural and effective consistency boundary.

The problem arises when we assume that the same fixed boundary should govern every decision involving a particular domain concept.

Think about a student.

Enrolling in a course requires information about the student and the course.

Changing the student's contact information requires neither course subscriptions nor capacity information.

Suspending a student's account might require yet another set of facts.

These operations concern the same student, but their consistency requirements differ.

With fixed aggregates, we need to decide upfront how to partition our consistency boundaries and then design operations around those partitions.

With DCB, each decision can define the relevant boundary dynamically.

Those boundaries may overlap. They may involve several domain concepts. They may even change as business requirements evolve.

And we don't have to reorganize an aggregate hierarchy every time a business decision needs to consider a different combination of facts.

That's a meaningful difference.

Does this mean we can forget about consistency design?

Absolutely not.

DCB moves some of the difficult design decisions. It doesn't make them disappear.

We still need to understand which events can affect a particular invariant.

If our consistency query omits an event that could invalidate the decision, we can still end up with incorrect outcomes.

If our query includes too many irrelevant events, we may introduce unnecessary conflicts and reduce concurrency.

We also need an event store capable of evaluating conditional appends atomically. Not every event store supports the necessary semantics.

And we need to design our events and tags carefully enough to make these queries reliable and efficient.

In the checkout example, this means being explicit about which commitments the local event store actually owns. If inventory is managed by an external warehouse, DCB cannot guarantee that stock remains available there. Such boundaries still require coordination, workflows, and failure handling.

There is another practical consideration.

A highly contended business rule remains highly contended, regardless of how elegantly we model it. DCB doesn't remove contention. It allows us to identify and protect the relevant boundary without forcing unrelated decisions to share it.

None of this is magic.

Good architecture still requires understanding the business, the data, and the guarantees we need to provide.

What this means for EventModeling

This is where I find the approach particularly compelling.

In EventModeling, we start by understanding the business story.

We identify what people do, what the system must decide, what facts are recorded, and how those facts influence subsequent actions.

We establish a shared understanding of the business process before committing to a particular implementation structure.

The resulting model gives us a sequence of events, commands, views, and automations. From there, we can identify vertical slices representing meaningful pieces of business functionality.

Now consider what happens when we implement those slices.

In a traditional aggregate-based architecture, we often introduce another modelling exercise. We identify aggregates, assign commands to them, and determine which streams own which data.

Sometimes this maps beautifully to the business model.

Other times, we find ourselves restructuring perfectly understandable business interactions to fit our aggregate boundaries.

DCB offers a different path.

For each state-changing slice, we can determine which events are needed to make the decision, build a purpose-specific projection, evaluate the business rules, and append the outcome conditionally.

That makes the relationship between the business model and its implementation much more direct.

Our decision models can remain small. They serve a particular purpose rather than representing the complete state of a long-lived domain object.

And because these models are derived from business decisions, they can evolve independently as those decisions change.

There is something appealing about that.

We spend considerable effort collaborating with domain experts to understand what the business actually needs. It seems reasonable to preserve that understanding as directly as possible when we implement the system.

The boundary was never the goal

I don't believe aggregates are obsolete. They have served us well, and they remain a useful way to organize state and enforce invariants.

But I do think we should question the assumption that consistency must always be tied to fixed aggregate streams.

When a business rule spans multiple domain concepts, we shouldn't automatically interpret that as an architectural problem requiring another coordination mechanism.

Perhaps the problem lies in the boundary we've chosen.

Dynamic Consistency Boundaries give us another way to think about this.

We can begin with the decision, identify the facts it depends on, and enforce the consistency those facts require.

Sometimes that boundary will look exactly like a traditional aggregate.

Sometimes it won't.

And that's fine.

Because ultimately, our responsibility isn't to protect aggregates.

It's to protect the correctness of business decisions.

Originally published on LinkedIn (2026-10-13): https://www.linkedin.com/pulse/consistency-belongs-decisions-aggregates-gabriel-n-schenker-sks7e