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:
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.