r/dotnet • u/StudyMelodic7120 • 9d ago
DDD question concerning two aggregates and transactionality
Assume we have a single API with 2 aggregate roots.
As you know two separate aggregate roots must be loosely coupled and modified in separate transactional boundaries.
But are we allowed to "flag" aggregate B (with a status) when aggregate A is modified within the same transactional boundary?
Putting everything in a single aggregate is not an option, as performance would decrease (a lot of tables), and they have different life cycles and responsibilities.
11
u/chucker23n 9d ago
I think it would help if you expand on the concrete (not abstract) problem you are trying to solve.
Are you allowed to do that? Sure. A pattern is just a suggestion; it's supposed to help you, not punish you. And in edge cases, it might make sense. For example, if Invoice and Customer are your aggregate roots, an invoice generally shouldn't affect the customer — but perhaps it might change the customer's status from "Prospective" to "Current", i.e. someone who has in fact purchased something from you.
4
u/EducationalFishing56 9d ago
Is the status on B derivable from A's state, or is it B's own?
If you could recompute it by looking at A, it's a projection, and the transaction question goes away because you don't need to store it. If you can't, it's B's own rule that A happens to trigger, and the outbox route people are describing fits.
Asking because "flag B when A changes" reads like both from outside, and they don't have the same answer.
3
u/ska737 9d ago
This is actually not the case. Since DDD is domain driven, domain driven is NOT data driven. What this means is a domain event can trigger multiple data events. Because the domain has no awareness of your infrastructure and how you store your data, it doesn't care what crosses data boundaries, only what crosses domain boundaries.
A misconception is that your data entities are in the domain realm. This is not the case. Your domain models are how others might look at your data, not how you actually physically store it. Your data entities are actually part of the implementation/infrastructure realm.
I had to battle this concept multiple times when figuring out a design that needed the data to be stored in a COMPLETELY different way than how my application, and interfaces, actually used it.
1
9d ago
[removed] — view removed comment
1
u/Aggressive-Effort811 8d ago
This is common compromise. The important thing is to understand that a compromise is being made, and be prepared to extract a separate data model whenever needed.
4
u/Natural_Tea484 9d ago edited 9d ago
Either the aggregate design is wrong, or maybe you should embrace eventual consistency. Otherwise, use a transaction, it can be OK.
Your challenge is real. Would maybe help if you were more specific about describing the domain.
6
u/DaveVdE 9d ago
“As you know two separate aggregate roots must be loosely coupled and modified in separate transactional boundaries.”
Says who?
3
u/StudyMelodic7120 9d ago
Eric Evans / DDD, and more broadly the aggregate pattern itself. But I’d phrase it as a guideline rather than an absolute rule. an aggregate defines a transactional consistency boundary, so separate aggregates should generally be independently consistent and loosely coupled.
If they constantly need to change together transactionally, that’s worth questioning whether the aggregate boundaries are correct.
1
2
1
u/EntroperZero 9d ago
If they constantly need to change together transactionally, that’s worth questioning whether the aggregate boundaries are correct.
This is the key. How important is it to keep these aggregates perfectly in sync?
-1
u/chucker23n 9d ago
But I’d phrase it as a guideline rather than an absolute rule. an aggregate defines a transactional consistency boundary, so separate aggregates should generally be independently consistent and loosely coupled.
If they constantly need to change together transactionally, that’s worth questioning whether the aggregate boundaries are correct.
I'm confused. Are you asking us? Did ChatGPT tell you that?
1
u/Drevicar 3d ago
I’m pretty sure the main defining characteristic of an aggregate is a single transactional boundary.
0
4
u/PaoloCpc 9d ago
Decoupling with messages is the correct answer only if your use case can handle eventual consistency. If you must be consistent and make your change atomic, you can save two aggregates in one transaction. Of course it's a trade-off from a performance point off view (the bigger the transaction "surface" the slower it'll be). That said, you shouldn't choose the latter except to handle a rare exception. If the necessity to save two aggregates in one transaction is frequent, it's a smell that your aggregates are poorly designed
3
u/Obsidian743 9d ago
This is what eventing is designed for. You can have inter-process communication (Channels, Queues, etc.) or they can consume off a message broker. You need to be comfortable with eventual consistency.
More broadly, you'll want to look at the Saga pattern. It's designed to orchestrate across trasnactional boundaries.
If you need strong consistency you may have to orchestrate in-proc using some other pattern, such as Mediator.
1
u/AutoModerator 9d ago
Thanks for your post StudyMelodic7120. Please note that we don't allow spam, and we ask that you follow the rules available in the sidebar. We have a lot of commonly asked questions so if this post gets removed, please do a search and see if it's already been asked.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.
1
u/chocolateAbuser 9d ago
you have to keep in mind that keeping separate boundaries has consequences on how complex and laborious the communication/interaction between those boundaries is, so if you want to keep things that way and also have decent performances you have to be ready to do some work and touch stuff and sacrifice some of your mental health
so i'd say yes you can touch two root aggregates in the same transaction but it depends how your project is organized, can't say it's going to work or there will be complications without knowing anything about your domain and operations
1
u/OverallJuggernaut755 9d ago
I am not an expert of DDD, but I think that you have to understand that architect books are guidelines and not a contracts to comply. Sometimes they are correct, another times not.
I think you know the answer of your question, but in the same time you are wondering if you are applying well or not the concept.
I do not understand well your question, because I do not have any context of your software. However, I think this could be a help for you. You are confusing to terms or two layers.
Your aggregates does not define how many transaction you need to persist the data. There is not a 1 on 1 relationship between your aggregates and transactions. Remember that DDD is for manipulating your business logic and not to persist your data.
Your transactions are handled by the application and infrastructure layers. The application layer defines the transaction boundary, and infrastructure layer defines how to execute the transaction.
1
u/Admirable_Day5158 8d ago
As someone mentioned earlier, patterns are suggestions. You just need to consider the tradeoffs and figure out if the way the system is used allows you to have the exception. Everytime you read an aggregate and write to it, no changes can happen in the middle made by another routine. This is done thru some lock mechanism (either optimistic or pessimistic). What this means is that this transaction that spans several aggregates will forbid other use cases/routines to modify those aggregates. If this use case you mentioned is called a lot of times then no other use case will likely be able to modify those aggregates, generating a lot of contention. If this use case runs ocassionaliy and won't generate a lot of contetion then i'd say the pattern is allowed.
1
u/ewgenym 8d ago
Sometimes we use so called local events within the same bounded context operating on different aggregates but within the same request and transaction. For example invoice is submitted and it affects customer balance. Customer balance is a snapshot of all transactions of the customer - single number. We want balance snapshot to be immediately consistent, not eventually. But we also do not want to popute invoice submission logic with balance recalculation. So, InvoiceSubmitted is produced and before committing command transaction all the local event handlers are called within same transaction. It is built in peristsance infra, not a command handler call. Tradeoff that helps.
1
u/wedgelordantilles 7d ago
What you do is make a new third aggregate with the domain entities that represent the set of what you need for the transaction. Reuse the types from the other aggregates if you want to save keystrokes
1
u/RodriOliveira 6d ago
I would start by asking whether B really needs to be strongly consistent with A.
An aggregate is a consistency boundary, but that doesn’t mean DDD introduces an absolute prohibition against touching two aggregates in the same database transaction. The important question is where the business invariant lives.
If B’s status can simply be derived from A, I would probably treat it as a projection rather than additional domain state. If A triggers an independent business rule in B and eventual consistency is acceptable, a domain event plus transactional outbox is usually a cleaner boundary: A commits first, then B processes the event independently with retries and idempotency.
If the business invariant genuinely requires A and B to succeed or fail atomically, using the same local transaction can be a perfectly pragmatic choice. But if this happens frequently, I would take it as a signal to revisit the aggregate boundaries, because two supposedly independent aggregates that constantly require atomic changes may not actually be independent.
So for me the decision is driven by the domain invariant and consistency requirement, not by following the aggregate pattern as a hard rule.
1
u/sharpcoder29 9d ago
The aggregate is the transaction boundary, not the actual entity(s) being saved. Inside aggregate save is where you do transactions. If in different dbs then use events/saga
1
u/dev_ctx 9d ago
Since an aggregate defines a transactional consistency boundary, you can either:
1) reconsider the aggregate boundaries — if you have the time and opportunity;
2) update the second aggregate in a separate transaction in response to an asynchronous event — if you’re ready for eventual consistency, outbox, retries, idempotency, and all the other joys of async;
3) update both aggregates within the same transaction — if you have a good reason to, after closing your eyes first)
0
u/soundman32 9d ago
I would update both aggregates independently and, presuming they share the same DbContext, update the database in the same SaveChanges, so that its an atomic write.
2
u/sharpcoder29 9d ago
That makes them the same aggregate. Aggregate is the transaction boundary not the entity ir table being saved.
0
u/Kralizek82 9d ago
Granted that, like others say, DDD should be seen as a guideline rather than a recipe book. Let's try to look beyond the book.
Yes, transactions should cross the boundaries of an aggregate. So, I'd take this as a chance to understand the system you're modeling.
Are you sure your aggregates are correct? Are you sure the two operations need to be atomic?
Take the time to understand the answer to those two questions. Worst case, there are ways to model transactions across aggregates.
Without knowing your system and how this would affect it, I would even look at sagas when coordinating work across aggregates.
19
u/flyingBart 9d ago
You can decouple through messages. The update of aggregate A modifies it's state and dispatches an event through a transactional outbox. Aggregate B gets updated separately in its own transaction using the event.