In a microservice, is the transactional boundary the bounded context or the aggregate?
May be the first question should be: what would help me decide whether my Microservice should contain an entire bounded context possibly with several aggregates, or should it rather contain only one aggregate?
If the Microservice contains several aggregates, then another question rises in my mind: shall the Microservice have several transactional boundaries (one per aggregate) or shall the Microservice have only one transaction boundary (the bounded context)?
Maybe, the answer to these questions is: it depends. But I’d like to have some rationale to make the right decisions.
I’ve tried to implement an experimental Microservice that implements a bounded context made of 3 aggregates and one transactional boundary per aggregate, and I’ve faced some pain points:
Eventual consistency within the Microservice itself
Atomic update of the aggregates and publication of domain events within the Microservice in a resilient way
Deciding whether all the domain events shall be published externally (to other Microservices) or only some of them
Deciding whether the domain events shall be published in the form of integration events in order to preserve the purity of the domain model
Then I’ve envisioned the possibility to split the Microservice into several Microservices, one per aggregate:
It gets more natural to publish all domain events (no clear need to make a difference between domain events and integration events)
The internal complexity of the Microservice is reduced
I’ve not managed to find some documentation on this subject to guide me in the decision making.
Would it make sense to consider that the bounded context could be a set of Microservices working together as partners, and where each Microservice would encapsulate one and only one aggregate with its own scaling strategy?
Then, each Microservice would have one and only one reason to change: the aggregate.
Of course the answer is always “it depends”. In this case though, most of the time, you would want a microservice to contain everything that is necessary for a closed set of functionalities. Note I said “functionalities”, as in business functions. Not technical functions, like saving a “Person” to a table, but business ones, like ‘transfer money from one account to the other’, ‘reserve seat’, things like that.
In your terms it would mean to contain one transactional boundary, and probably multiple “aggregates”.
All of this is because of maintainability. When you modify a given functionality (or fix a bug, etc.) you will want that to be localized to a single microservice. Anything that requires coordination between microservices is usually much more difficult. You have to coordinate teams, coordinate rollout, etc., and that is exactly what microservices are supposed to be a solution for.
You can usualy (it depends, as allways) map one bounded context to one microservice.
A microservice can use several aggregates.
The transaction boundary is the operation that the aggregate performs. This operation can not be rejected/undo/rollback by anything outside the aggregate. If the aggregate say “YES” it is because ALL business rules and invariant are checked. Having to reject/undo/rollback an aggregate operation from outside the aggregate is a smell for wrong design.
HINT: Asides of purely wrong designs, fine grained operations used to be the solution of many potentialy “undo” encounters. A user does not “Pay” when push Pay button. Becasue you have to undo the registered “Pay” operation (the aggregate says that, as far as it knows, the user can pay) if the bank reject the operation. A user raises the “Pay” business operation that consist in several steps:
“Order a payment”: register the order and raise UserOrderedPayment event if the aggregate says “OK” to that. After that, everything else is internal events/operations in your sistem.
UserOrderedPayment event launch the charge of money. If it fails, UserOrderPaymentRejected event is raised and the Payment Order is marked as rejected. If the money charges with no problem then a new Payment is registered and UserPaid event is raised.
Eventual consistency within the Microservice itself
With a fine grained design as above; there is no eventual consistency within the MS itself because every step is in a consistent status. The payment order could be still in pending status (but it is a valid and consistent status), OK, just wait a few milliseconds and refresh ;-).
Atomic update of the aggregates and publication of domain events
within the Microservice in a resilient way
Again; with a fine grained design as above; the transacion boundary is just one aggregate so there is no “atomic update of the aggregates“. As domain events (within the same MS or not) should not be rejected, they only fails by infrastructure downs (net, persistence, etc) so the best strategy is keep the failed events somewhere and raise them when the outage is over.
Deciding whether all the domain events shall be published externally
(to other Microservices) or only some of them
You should invert the responsibility here. Always raise the domain events and provide a way to subscribe to that events. This way the resposibility lays on the interested microservice that has to subscribe to the events needed.
Deciding whether the domain events shall be published in the form of
integration events in order to preserve the purity of the domain model
You only need raise the same domain events, you integration code can subscribe to these events and perform the integration action.
Assuming we follow the approach of one microservice for each BC, the transactional boundary is the microservice. Each application service method of the BC would be a transaction. An aggregate inside the BC define a group of domain objects that should change together in order to maintain invariants. An operation on an aggregate should be wrapped into a transaction. So each application service method should call just one aggregate operation.