Consistency is important in software. It is a problem if an order is created without payment information, if a subscription is renewed but access entitlements are not granted, or if a refund is completed but the settlement amount remains unchanged.
So we want to handle everything inside one transaction. It feels reassuring if one request renews the subscription, saves the payment, grants entitlements, sends a notification, and creates settlement data.
But the moment we try to handle everything at once, the model grows and the system becomes tightly coupled. This is why transactions and consistency must be considered together when talking about Aggregates in DDD.
The important question is not “Do we need consistency?” It is “What must be kept consistent at once?”
There are different kinds of consistency
When we hear consistency, we often think of all changes inside one transaction succeeding or failing together. This can be seen as strong consistency.
For example, the state and period of a subscription may need to change together. It would be strange if a subscription were renewed but its expiration date stayed the same. It is natural to protect this kind of rule in one transaction inside the same Aggregate.
subscription.renew(paymentResult, now);
subscriptionRepository.save(subscription);
Notifications are different. If a notification is sent a few seconds after a subscription is renewed, there may be no major domain problem. Settlement data can also be handled by batch or events if it does not need perfect real-time consistency.
In these cases, eventual consistency can be used. That is, the system is not immediately consistent, but reaches a consistent state over time.
Aggregate is the boundary of strong consistency
When designing an Aggregate, the important criterion is invariants. We need to think about which rules must be protected and whether those rules must be guaranteed inside one transaction.
Suppose the following rules must be protected inside the subscription Aggregate:
The expiration date cannot be before the start date.
A canceled subscription cannot be canceled again.
A free trial subscription must have a trial end date.
A subscription in a grace period must have a renewal failure reason.
These rules are invariants about the subscription’s own state. Therefore it is natural to protect them strongly inside the Subscription Aggregate.
On the other hand, the following rules can involve other Aggregates or systems:
When payment succeeds, the subscription should be renewed.
When a subscription is renewed, access entitlements should be granted.
When a subscription is canceled, a notification should be sent.
Subscription change history should be reflected in the settlement system.
These rules are important, but we must examine whether they must be inside the same transaction. If all of them go into the same Aggregate, Subscription can become a giant object responsible for payment, entitlements, notifications, and settlement.
The cost of putting everything in one transaction
When several Aggregates and external systems are processed together inside one transaction, several problems appear.
First, the transaction becomes long. Database locks are held longer, affecting performance and concurrency.
Second, external system calls and DB transactions mix. A payment API call can succeed while DB saving fails. Conversely, message publishing can fail after DB saving succeeds.
Third, the model grows. To guarantee consistency at once, we want to put all related objects inside the same boundary. Then Aggregates become large and change conflicts increase.
Fourth, failures propagate. We need to decide whether a subscription renewal itself should fail because the notification system is down.
Protecting consistency strongly has a cost. We must judge whether a rule is important enough to pay that cost.
Eventual consistency is not giving up
Choosing eventual consistency does not mean giving up consistency. It means matching things across time and boundaries instead of matching everything immediately.
For example, when subscription renewal succeeds, we can publish a SubscriptionRenewed event.
subscription.renew(paymentResult, now);
subscriptionRepository.save(subscription);
eventPublisher.publishAll(subscription.pullEvents());
The entitlement context and notification context can receive and process this event.
SubscriptionRenewed
-> Entitlement context: grant access entitlements
-> Notification context: send renewal completion notification
-> Settlement context: record target for revenue reflection
Subscription renewal itself is handled strongly inside the transaction of the subscription context. Entitlement granting and notification sending are handled later through events. This approach separates the models of each context while allowing the overall system to eventually reach a consistent state.
Of course, eventual consistency requires operational considerations. We need to design for event publishing failure, duplicate processing, retries, ordering problems, and compensation. It is not an easy choice. But putting everything in one transaction is also not an easy choice.
What must be protected at once?
When deciding transaction boundaries, we can ask:
Is it unacceptable for this rule to be broken even briefly?
Must these objects always change together?
Is this rule an invariant inside one Aggregate?
Can it be rolled back if it fails?
Is it okay if it becomes consistent a few seconds or minutes later?
Can another context handle it independently?
For example, the rule “the end date of a subscription period cannot be before the start date” must not be broken even briefly. It should be guaranteed immediately inside the Aggregate.
On the other hand, the rule “an email notification should be sent after subscription renewal” may be okay if delayed by a few seconds. Event-based follow-up processing can be appropriate.
“Renew subscription after payment succeeds” needs more careful handling. If payment succeeds but subscription renewal fails, it creates a serious problem for the customer. But an external payment system and a database cannot be tied into one local transaction. In this case, designs such as idempotency, retry, payment result lookup, and compensation are needed.
Domain Event and transactions
Domain Event is useful for expressing follow-up processing that crosses transaction boundaries. When an important event happens inside an Aggregate, it can be recorded as an event and published after the transaction succeeds.
What matters is when the event is published. If an external message is published before the DB transaction commits, DB saving may fail while the external system has already received the event. Conversely, if publishing fails after commit, state has changed but follow-up processing is missing.
Patterns such as the Outbox pattern can be used to handle this problem. The key is that event publishing is also part of consistency design.
Using events does not automatically make things safe. But events can be a good tool for expressing “what should be protected now, and what should be matched later.”
Closing
Transaction is a technical feature, but transaction boundary is a domain design problem. We must decide what must be protected at once and what can be matched later.
Aggregate is the boundary that protects strong consistency. It is not a unit that gathers all related objects, but a boundary of invariants that must be protected together.
Eventual consistency is not giving up consistency. It is a way to split boundaries and reach the correct state over time through events and retries. Of course, it brings operational and design responsibilities.
What matters in DDD is neither always creating large transactions nor always splitting everything into events. What matters is distinguishing the kind of consistency based on domain rules.
What must be protected now? What can be matched later? When we answer these questions, Aggregate and transaction boundaries become clearer.