1
votes

Let's consider the flow below:

  • API client calls [POST] /api/v1/invitation/:InvitationId/confirm
  • Confirm the invitation within a SAGA
  • Eventually raise an InvitationConfirmed event to indicate success

We are some troubles finding a good place to validate the "event" we pass to the SAGA. For instance, we want to make sure that: - The specified InvitationId exists - The corresponding invitation is not expired or already processed

We tried a couple of things:

  1. Fire a command:

    • Fire a command RequestInvitationConfirmation
    • Handle synchronously this command and return an error if the command is not valid OR otherwise raise the InvitationConfirmationRequested event.

The rest of the flow is the same

CONS: - Requires us to follow a "request/response" pattern (synchronous within the HTTP request lifetime)

  1. Raise an event:

    • Raise an event InvitationConfirmationRequested
    • Within the SAGA, query the Invitation service and perform the validations. If the command is not valid, we publish an event InvitationConfirmationFailed (...)

CONS: - As far as I understand SAGA should be used to orchestrate the flow. Here we are introducing the concept of "validation". I'm not sure it's the recommended approach.

Validation is a very common concept. How would you handle it in a distributed fully asynchronous system?

2

2 Answers

1
votes

Important point in the design of this system is: "Who is the client of this API?".

  • If this client is an internal Service or Application that's one thing (as in a distributed app, microservices etc.).
  • If the API is used by third party client's, that's another thing.

Short answer

If the API is used internally between Services, sending a command with invalid Id in the system is a fault, so it should be logged and examined by the system developers. Also cases like these should be accounted for by having a manual way of fixing them (by some administrative backend). Log these kinds of stuff and notify developers.

If the API is used from third party apps, then it matters how responsibilities are separated between the API and the other part of the system that it uses. Make the API responsible for validation and don't send commands with invalid id's. Treat command with invalid ID's like fault, as in the first case. In this case if you use asynchronous flow, you will need a way to communicate with the third party app to notify it. You can use something like WebHooks.

For the second part of the validations check these series of blog posts and the original paper.

Long answer

If you search around you will see a lot of discussions on errors and validations, so here's my take on that.

Since we do separation of other parts of our systems, it's seems natural to separate the types of error that we have. You can check this paper on that topic.

Let's define some error types.

  • Domain Errors
  • Application Errors
  • Technical Errors (database connections lost etc.)

Because we have different types of errors, the validation should be performed from different parts of our systems.

Also the communication of these errors can be accomplished by different mechanisms depending on:

  • the requester of the operation and the receiver
  • the communication channel used
  • the communication type: synchronous or asynchronous

Now the validations that you have are:

  • Validate that an Invitation with the specified Id exists
  • Validate that the Invitation has not expired
  • Validate that the Invitation is not already processed (accepted, rejected etc.)

How this is handled will depend on how we separate the responsibilities in our application. Let's use the DesignByContract principle and define clear rules what each layer (Domain, Application etc.) should expect from the other ones.

Let's define a rule that a Command containing an InvitationId that doesn't correspond to an existing Invitation should not be created and dispatched.

NOTE the terminology used here can vary vastly depending of what type of architecture is used on the project (Layered Architecture, Hexagonal etc.)

This forces the CommandCreator to validate that an Invitation exists with the specified Id before dispatching the command.

In the case with the API, the RouteHandler (App controller etc.) that will accept the request will have to:

  • perform this validation himself
  • delegate to someone else to do the validation

Let's further define that this is part of our ApplicationLayer (or module, components etc. doesn't matter how it's called, so I'll use Layer) and make this an ApplicationError. From here we can do it in many different ways.

One way is to have a DispatchConfirmInvitationCommandApplicationService that will ask the DomainLayer if an Invitation with the requested Id exists and raise an error (throw exception for example) if it doesn't. This error will be handled by the RouteHandler and will be send back to the requester.

You can use both a sync and async communication. If it's async you will need to create a mechanism for that. You can refer to EnterpriseIntegrationPatterns for more information on this.

The main point here is: It's not part of the Domain

From here on, everyone else in our system should consider that the invitation with the specified Id in the ConfirmInvitationCommand exists. If it doesn't, it's treated like a fault in the system and should be checked by developers and/or administrators. There should be a manual way (an administrative backend) to cancel such invalid commands, so this must be taken into account when developing the system, bu treated like a fault in the system.

The other two validations are part of the Domain.

So let's say you have a

  • Invitation aggregate
  • InvitationConfirmationSaga

Let's make them these aggregates communicate with messages. Let's define these types of messages:

  • RequestConfirmInvitation
  • InvitationExpired
  • InvitationAlreadyProcessed

Here's the basic flow:

  • ConfirmInvitationCommand starts a InvitationConfirmationSaga

  • InvitationConfirmationSaga send RequestConfirmInvitation message to Invitation

And then:

  • If the Invitation is expired it sends InvitationExpired message to InvitationConfirmationSaga

  • If the Invitation is processed it sends InvitationAlreadyProcessed message to InvitationConfirmationSaga

  • If the Invitation is not expired it, it's accepted and it sends InvitationAccepted message to InvitationConfirmationSaga

Then:

  • InvitationConfirmationSaga will receive these messages and raise events accordingly.

This way you keep the domain logic in the Domain, in this case the Invitation Aggregate.

0
votes

You have a command ConfirmInvitation containing InvitationId. You send it to your Invitation domain from InvaitationAppService. Your Invitation domain should look like this

...
public void ConfirmInvitation()
{
    if (this.Status == InvitationStatus.Confirmed)
        throw new InvalidInvitationException("Requested invitation has already been confirmed");
    //check more business logic here
    this.Status = InvitationStatus.Confirmed;
    Publish(new InviationConfirmedEvent(...));
}
...

Your InvitationAppService should have something like below:

...
public void ConfirmInvitation(Guid invitationId)
{
    // rehydrate your domain from eventstore
    var invitation = repo.GetById<Invitation>(invitationId);
    if (invitation == null)
        throw new InvalidInvitationException("Invalid Invitation requested");
    invitation.ConfirmInvitation(new ConfirmInvitation(...));
}

You don't need to introduce a new event InvitationConfirmationRequested. DDD is an approach in which your domain/business validation should reside inside domains. Don't try to fit other patterns or technologies in your domain. Validating your domain inside saga(which is used to orchestrate distribute transactions across the services) might create complexities and chaos