Search code examples
javaeventsasynchronousdomain-driven-designeai

Strategies to call other bounded context


I'm currently on a study project involving Domain-driven design (DDD) and integration scenarios of multiple domains.

I have a use case in one of my bounded context where I need to contact another BC to validate an aggregate. In fact, there could be several BC to ask for validation data in the future (but no for now).

Right now, I'm suffering from DDD obsessive compulsive disorder nervous breakdown where I cannot found a way to apply patterns correctly (lol). I would really appreciate some feedback from people about it.


About the 2 bounded contexts.
- The first one (BC_A) where the use case is taking place would contain a list of elements that are related to the user.
- The external one (BC_B) has some knowledge about those elements

* So, a validation request from BC_A to BC_B would ask a review of all elements of the aggregate from BC_A, and would return a report containing some specifications about what to do with those elements (if we should keep it or not, and why).
*The state of the aggregate would pass through (let say) "draft" then "validating" after a request, and then depending on the report sent back, it would be "valid" or "has_error" in case there is one. If the user later choose to not follow the spec, it could pass the state of the aggregate to "controlled" meaning there is some error but we do not taking care of it.

The command is ValidateMyAggregateCommand

The use case is:

  1. get the target aggregate by id
  2. change its state to "validating"
  3. persist the aggregate
  4. make validation call (to another BC)
  5. persist the validation report
  6. acknowledge the validation report with the target aggregate (which will change its state again depending on the result, should be "OK" or "HAS_ERROR")
  7. persist the aggregate again
  8. generate a domain event depending on the validation result

it contains 8 steps, possibly from 1 to 3 transactions or more.


I need to persist the validation report localy (to access it in the UI) and I think I could do it:

  • after the validation call independently (the report being its own aggregate)
  • when I persist the target aggregate (it would be inside it)

I prefer the first option (step 5) because it is more decoupled - even if we could argue that there is an invariant here (???) - and so there is a consistency delay between the persistance of the report and the acknownledgement by the aggregate.


I'm actually struggling with the call itself (step 4).

I think I could do it in several ways:

  • A. synchronous RPC call with REST implementation
  • B. call without a response (void) (fire and forget) letting several implementations options on the table (sync/async)
  • C. domain event translated into technical event to reach other BC

A. Synchronous RPC call

// code_fragment_a
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

ValidationReport report = validationService.validate(myAggregate);  // #4
validationReportRepository.save(report);                            // #5

myAggregate.acknowledge(report);                                    // #6
myAggregateRepository.save(myAggregate);                            // #7
// ---

The validationService is a domain service implemented in the infrastructure layer with a REST service bean (could be local validation as well but not in my scenario).

The call needs a response immediately and the caller (the command handler) is blocked until the response is returned. So it introduces a high temporal coupling.

In case the validation call fails because of technical reasons, we take an exception and we have to rollback everything. The command would have to be replayed later.

B. Call without response (sync or async)

In this version, the command handler would persist the "validating" state of the aggregate, and would fire (and forget) the validation request.

// code_fragment_b0
// = ValidateMyAggregateCommandHandler
// ---
myAggregate = myAggregateRepository.find(command.myAggregateId());  // #1
myAggregate.changeStateTo(VALIDATING);                              // #2
myAggregateRepository.save(myAggregate);                            // #3

validationRequestService.requestValidation(myAggregate);            // #4
// ---

Here, the acknowledgement of the report could happen in a sync or async manner, inside or outside the initial transaction.

Having this code above in a dedicated transaction allows failures in validation call to be harmless (if we have a retry mechanism in the impl).

This solution would allow to start with a sync communication quickly and easily, and switch to an async one later. So it is flexible.

B.1. Synchronous impl

In this case, the implementation of the validationRequestService (in the infrastructure layer) does a direct request/response.

// code_fragment_b1_a
// = SynchronousValidationRequestService
// ---
private ValidationCaller validationCaller;

public void requestValidation(MyAggregate myAggregate) {
        ValidationReport report = validationCaller.validate(myAggregate);

        validationReportRepository.save(report);
        DomainEventPublisher.publish(new ValidationReportReceived(report))
}
// ---

The report is persisted in a dedicated transaction, and the publishing of an event activate a third code fragment (in the application layer) that do the actual acknowledgment work on the aggregate.

// code_fragment_b1_b
// = ValidationReportReceivedEventHandler
// ---
public void when(ValidationReportReceived event) {
        MyAggregate myAggregate = myAggregateRepository.find(event.targetAggregateId());
        ValidationReport report = ValidationReportRepository.find(event.reportId());

        myAggregate.acknowledge(report);                                        
        myAggregateRepository.save(myAggregate);
}
// ---

So here, we have an event from infra layer to the app layer.

B.2. Asynchronous

The asynchronous version would change the previous solution in the ValidationRequestService impl (code_fragment_b1_a). The use of a JMS/AMQP bean would allow to send a message in a first time, and receive the response later independently.

I guess the messaging listener would fire the same ValidationReportReceived event, and the rest of the code would be the same for code_fragment_b1_b.

As I write this post, I realize this solution (B2) present a nicer symetry in the exchange and better technical points because it is more decoupled and reliable regarding the network communications. At this point it is not introducing so much complexity.

C. Domain events and bus between BCs

Last implementation, instead of using a domain service to request a validation from other BC, I would raise a domain event like MyAggregateValidationRequested. I realize it is a "forced" domain event, ok the user requested it but it never really emerge in conversation but still it is a domain event.

The thing is, I don't know yet how and where to put the event handlers. Should the infrastructure handlers take it directly ?

Should I translate the domain event into a technical event before sending it to its destination ???

technical event like some kind of DTO if it was a data structure


I guess all the code related to messaging belong to the infrastructure layer (port/adapter slot) because they are used to communicate between systems only.

And the technical events that are transfered inside those pipes with their raising/handling code should belong to the application layer because like commands, they end up on a mutation of the system state. They coordinates the domain, and are fired by the infra (like controllers firing application service).

I read some solutions about translating events in commands but I think it makes the system more complex for no benefits.

So my application facade would expose 3 types of interacion: - commands - queries - events

With this separation, I think we can isolate commands from UI and events from other BCs more clearly.


Ok, I realize that post is pretty long and maybe a little bit messy, but this is where I'm stuck, so I thank you in advance if you can say something that could help me.

So my problem is that I'm struggling with the integration of the 2 BC.
Different solutions:
- The service RPC (#A) is simple but limit the scale,
- the service with messaging (#B) seems right but I still need feedback,
- and the domain events (#C) I don't know really how to cross boudaries with that.

Thank you again!


Solution

  • I have a use case in one of my bounded context where I need to contact another BC to validate an aggregate.

    That's a really weird problem to have. Typically, aggregates are valid, or not valid, entirely dependent on their own internal state -- that would be why they are aggregates, and not merely entities in some larger web.

    In other words, you may be having trouble applying the DDD patterns because your understanding of the real problem you are trying to solve is incomplete.

    As an aside: when asking for help in , you should adhere as closely as you can to your actual problem, rather than trying to make it abstract.

    That said, there are some patterns that can help you out. Udi Dahan walks through them in detail in his talk on reliable messaging, but I'll cover the high points here.

    When you run a command against an aggregate, there are two different aspects to be considered

    • Persisting the change of state
    • Scheduling side effects

    "Side effects" can include commands to be run against other aggregates.

    In your example, we would see three distinct transactions in the happy path.

    The first transaction would update the state of your aggregate to Validating, and schedule the task to fetch the validation report.

    That task runs asynchronously, querying the remote domain context, then starts transaction #2 in this BC, which persists the validation report and schedules a second task.

    The second task - built from the data copied into the validation report - starts transaction #3, running a command against your aggregate to update its state. When this command is finished, there are no more commands to schedule, and everything gets quiet.

    This works, but it couples your aggregates perhaps too tightly to your process. Furthermore, your process is disjoint - scattered about in your aggregate code, not really recognized as being a first class citizen.

    So you are more likely to see this implemented with two additional ideas. First, the introduction of a domain event. Domain events are descriptions changes of state that are of special significance. So the aggregate describes the change (ValidationExpired?) along with the local state needed to make sense of it, publishing the event asynchronously. (In other words, instead of asynchronously running an arbitrary task, we run asynchronously schedule a PublishEvent Task, with an arbitrary domain event as the payload).

    Second, the introduction of a "process manager". The process manager subscribes to the events, updates its internal state machine, and schedules (asynchronous) tasks to run. (These tasks are the same tasks that the aggregate was scheduling before). Note that the process manager doesn't have any business rules; those belong in the aggregates. But they know how to match commands with the domain events they generate (see the messaging chapter in Enterprise Integration Patterns, by Gregor Hohpe), to schedule timeout tasks that help detect which scheduled tasks haven't completed within their SLA and so on.

    Fundamentally, process managers are analogous to aggregates; they themselves are part of the domain model, but access to them is provided to them by the application component. With aggregates, the command handler is part of the application; when the command has been processed by the aggregate, it's the application that schedules the asynchronous tasks. The domain events are published to the event bus (infrastructure), and the application's event handlers subscribe to that bus, loading the process managers via persistence, passing the domain event to be processed, using the persistence component again to save the updated process manager, and then the application schedules the pending tasks.

    I realize it is a "forced" domain event, ok the user requested it but it never really emerge in conversation but still it is a domain event.

    I wouldn't describe it as forced; if the requirement for this validation process really comes from the business, then the domain event is a thing that belong in the ubiquitous language.

    Should I translate the domain event into a technical event before sending it to its destination

    I have no idea what you think that means. Event is a message describing something that happened. "Domain event" means that the something happened within the domain. It's still a message to be published.