What is an Aggregate?
An aggregate is extracted as a unit to maintain invariant conditions and brings order to the operation of objects. It consists of boundaries and a root. The boundary of an aggregate defines what is included in the aggregate. The root of an aggregate is a specific object within it. All external operations on the aggregate are conducted via the aggregate root. By not exposing objects within the aggregate boundary to the outside, the invariant conditions within the aggregate are maintained.
It is reasonable to consider aggregates as units of the repository
e.g.
// aggregates
package repository
type UserRepository interface {
Create(user entity.User) error
Delete(userId int) error
}
Transaction Boundaries
Scope of a Transaction.
In DDD, it is ideal to consider an aggregate should be the same as a transaction boundary.
Disadvantages of spanning transactions across aggregates:
- Spanning a transaction across aggregates A and B expresses the start and end of the transaction in the application layer, obscuring the consistency level of each aggregate.
- Technical changes (like ORM or storage changes) necessitate modifications in the use case/domain layer, leading to fragile code.
- Spanning aggregates naturally widens the transaction scope, increasing the likelihood of db locks.
e.g.
This sample code hides the creation/commit/rollback of tx in infrastructure, but is fragile due to its dependency on the db client in the use case/domain layer. It is sort of anti-pattern. will explain later.
// Infrastructure layer
func (t *TransactionRepository) Do(ctx context.Context, runner func(tx *ent.Tx) error) error {
// Start transaction
tx, err := t.client.Tx(t.ctx)
if err != nil {
return err
}
err = runner(tx)
if err != nil {
// Rollback process
if err := tx.Rollback(); err != nil {
return err
}
return err
}
// Commit process
if err := tx.Commit(); err != nil {
return err
}
return nil
// Usecase layer
func (u *lessonUseCase) TransactionSample(ctx context.Context) error {
// Has dependency with db client
err := u.TransactionRepository.Do(ctx, func(tx *ent.Tx) error {
// Transaction processing here
newUser, err := userRepo.create(tx, ×××)
if err != nil {
return err
}
err := userGroupRepo.create(tx, newUser, ×××)
return nil
})
if err != nil {
return err
}
return nil
}
In real-world project, Of course, there are many challenging cases:
- e.g. Creating a User and also wanting to update a Organization etc...
Solutions
1. Merge Aggregates
If crossing aggregates with tx is problematic, merge them together (as seen in some DDD books).
Advantages:
- Maintains consistency within the aggregate Disadvantages:
- Larger aggregates might lead to performance issues due to increased db locks
- Merging different aggregates into one (e.g., A and B into C) complicates the code
- Application logic becomes more infrastructure-dependent. Hard to debug and loos testability.
2. Eventual Consistency
Accept temporary data breaches.
Advantages:
- No need to change aggregates - Transaction boundaries remain within existing aggregates, keeping the scope narrow
Disadvantages:
- Temporary loss of consistency
- Implementing measures for maintaining consistency can be cumbersome
- Handling failures in mid-process (e.g. queue)
- e.g. if creating a user succeeds but creating a organization fails, the user might be removed
- Handling failures in recovery
3. Transaction Across Aggregates
This is an anti-pattern.
Advantages:
- Maintains consistency
- No need to modify aggregates
Disadvantages:
- Fragile to changes
- Wider transaction scope may lead to locks
Summary
This post delves into the intricacies of aggregate transaction boundaries within DDD, highlighting the balance between consistency, performance, and system design. Practical solutions and their trade-offs are discussed, providing insights for effective DDD implementation in real-world scenarios.
Honestly, I am still confused and don't know solution. It is really depends on you and your team.
Also I did not mention about modeling in this time. I think modeling is key factor to find better solution.
Happy coding!
Top comments (0)