CQRS - where should I place requests to external systems?

Viewed 1301

I'm wondering where requests to external systems (to be specific: a Webservice) should be placed in a CQRS-based system.

For example, given a system that sends a booking-request to an external flight service: Should this be in the domain object, in the command handler for "bookFlight"? Or should this be in a saga, as a reaction to a domain object event "flightBookingPlaced"?

2 Answers

I'll make some assumptions:

  • The external request is part of the "transaction".
  • The external request is core to the behaviour of the command.
  • The external system response is synchronous so much as it either responds or fails, there are no callbacks or polling involved.

I would say it can belong in the command or as a series of commands.

Hide the external service behind an ACL or facade, make that a dependency on the command. The command will then represent the transition from "not booked" to "booked". Ignoring the complexities of the command "blocking" until complete (effectively), that'll cover what you need.

If you wanted to support a more granular approach, the small series of commands approach feels like it fits best:

not booked -> booking pending -> booked

Launch the event and trigger a RequestBookingCommand, which changes the booking state from "not booked" to "booking pending", and commits the transaction. This can then trigger the next command ExternalBookingCommand, which can work in the background without needing the domain object initially. The booking can be performed on the external system and if successful, take you from "booking pending" to "booked". If it fails, you can retry or revert the booking to "booking failed".

This then at least allows you to start putting validation around not attemping to double book etc.

I can't speak to sagas specifically, but I would like to think you could represent the protocol of "booking commands" as a little saga; mapping you from one domain state (not booked) to the eventual state (booked) with as many stops as you need inbetween.

In either approach, what is important is defending domain state and ensuring any transactions are integral. Going more granular with the states and events might help also because you can use better language (one of DDD's tenets) to describe what is occurring, such as RequestBookingCommand leaving you in a BookingRequested state, following onto a PerformExternalBooking command starting with a BookingRequested state and leaving you in a Booked or BookingFailed state. You can also then introduce domain events such as SuccessfullyBooked or BookingRequestedOnFoo.

My approach to these situations, usually, is to try not to overthink it and first build a model that matches how I describe it verbally. Frameworks and infrastructure can help you combat technical considerations (such as transactions or concurrency).

If this is not an internal microservice - really fast and stable, I would do this in Saga/Process Manager/Gateway - an async actor with its own state machine. With external services you would want to have error processing, retries, timeouts - everything async, so your aggregate is not blocked.

Related