ACK/NACK for Commands with CQRS
How to signify acknowledgement, or negative-acknowledgement, of Commands when applying CQRS.
What is ACK/NACK
ACK/NACK are a set of protocol messages exchanged between systems to ensure communication happens consistently and accurately.
ACK (short for Acknowledgement) is an indication the message was received fine and understood. Alternatively, NACK (short for Negative Acknowledgement) indicates something went wrong.
Essentially, it’s a handshake between two systems.
An example of this can be HTTP status codes. Server responses contain a status code to indicate the outcome of the client’s request. The calling application can use the status code to determine what to do next.
Let’s explore how we can utilise ACK/NACK for Commands in CQRS.
Responsibility
Let’s assume we have a layered architecture. We can expect to see both Domain and Application layers. There is one more - the Framework layer. You may be more familiar with calling this layer UserInterface, or Presentation. Layering our system helps us to maintain clear separation of concerns.
Domain layers are responsible for encapsulating the business logic and rules for a system. Application provides a Service Layer which orchestrates the Domain with Commands and Command Handlers. Framework glues the delivery mechanism (HTTP, Console, CRON, etc.) and the Application.
In which layer should we place the decision to return an ACK or NACK?
Should it be within the Domain layer? No. Agreed? Good. Since we’re all in agreement, let’s move up a layer.
Should it be within the Application layer? No.
Layered architectures have an inward direction of dependency. Inner layers are unaware of those above - and are not dependent upon them. The Domain layer is unaware of the Application layer. The Application layer is unaware of the Framework layer. As a result, our Application layer is agnostic to the delivery mechanism. It has no idea if it is being invoked from a REST API call, handling a job from a queue or being run as part of an integration test.
Our ACK/NACK message depends upon the mode of communication. If our system is handling a successful HTTP request, we may wish to Acknowledge with a `200 OK` response. This would not make sense if the system is being interacted with from the console. Perhaps, in this case, we may with to output some formatted green text to represent the successful request along with a successful exit code.
The Application layer may be agnostic to the delivery mechanism, but the Framework layer is fully aware. The responsibility to decide if/when we return an ACK or NACK belongs within the layer most knowledgable about the delivery mechanism.
It belongs in the Framework layer.
CQRS Commands
Commands represent intent from the user to perform behaviour within the system. The objects are DTOs encapsulating the required parameters needed to perform the behaviour within a Command Handler.
There is a lot of conflicting advice surrounding Command Handlers and if they should return values. The original paper states:
“If you mutate state your return type must be void”
— Greg Young
Based on this advice, since Command Handlers mutate state, they should have a void return type. This means we will not have a result within the Framework layer to determine if an ACK/NACK should be returned.
This is perfectly fine - a result is not needed.
Making the decision to return an ACK or NACK has nothing to do with the Command being handled. The protocol messages represent a successful understanding of the request - not the execution.
We may decide not to pass Commands directly to their Handler, but instead, pass it to a Command Bus which manages the transport. From the perspective of the Framework layer, all it knows is it passed a message into the bus. That’s it.
The Command may reach it’s Handler - but it may not. It may be executed synchronously, or queued. We may simply want Commands to always be asynchronous. It may be sent to a Dead Letter Queue to be retried after a network issue. In fact, there are any number of reasons why a Command may not reach it’s Handler and, therefore - we couldn’t rely on a result even if we wanted to.
What we aim to communicate with ACK/NACK is a successful understanding. We perform validation which does not require data from the Domain Model. For Commands in CQRS we are Acknowledging the Command looks valid.
If no exceptions were thrown during the creation of the Command, or passing it to the Command Bus, then we can ACK. Otherwise, we catch the exceptions and respond with NACK.
Command Validation
Mathias Verraes wrote an excellent article explaining the 3 stages of validation:
Form: Happens client-side to provide good UX. Can’t be trusted.
Command: Checks if the Command looks valid. Doesn’t check set constraints.
Model: Enforces business rules during Command execution using domain objects.
Command validation will help us decide if we should return an ACK or NACK. We need to check if the Command looks valid - but what does that mean?
We are not concerned with deciding if a Command should be executed. Instead, we are more concerned with ensuring the Command has all the pieces of data which are required and the values are consistent with the rules of the business.
Imagine the UI for a simple form where you can sign up for a newsletter. It accepts an email address. The `NewsletterSignUp`
Command will expect an email address:
final class NewsletterSignUp
{
public function __construct(
public readonly string $emailAddress
) {}
}
If we try to create an instance of `NewsletterSignUp`
without providing an email address, we will be presented with an error:
PHP Fatal error: Uncaught ArgumentCountError: Too few arguments to function NewsletterSignUp::__construct(), 0 passed in ack-nack.php on line 10 and exactly 1 expected in ack-nack.php:5
Errors thrown when unable to create instances of the `NewsletterSignUp`
Command can be caught and a NACK can be returned to the client. We can continue to model what a valid Command looks like by encapsulating all the rules which must be true before an instance of the Command can be created.
At the moment `$emailAddress`
is a string. This means we can still create an invalid `NewsletterSignUp`
because we are not validating any values. Instead of checking values within the Command’s constructor, we delegate validation to Value Objects within our Domain:
final class NewsletterSignUp
{
public function __construct(
public readonly EmailAddress $emailAddress
) {}
}
The Command object should use Value Objects. They guarantee their own consistency, so the Command delegates validation to them. Note that we’re not trying to inform the user of validation errors. We simply throw exceptions.
— Mathias Verraes
A common challenge to this idea is: “we do not want to leak the Domain”.
Delegating to Value Objects do require us to reach into our Domain layer from multiple layers above. Framework-aware code, responsible for mapping a delivery mechanism to the Application layer, is now aware of the existence of Domain objects. Our Controllers, Console Commands and Cron jobs now create instances of Value Objects.
Skipping the Application layer and reaching into the Domain layer is perfectly fine. As long as the direction of dependency is inward, the inner layers remain agnostic to the outer layers and avoid coupling, retain single responsibility and are still easily tested in isolation.
Value Objects encapsulate rules of the business. They belong within the Domain layer. They are a source of truth. If we decide to make a change to what a valid `EmailAddress`
is - we should have one place to change.
Imagine if we are required to update our code reject emails from @gmail.com. We may choose to codify this within the `EmailAddress`
Value Object. However, if we have many Commands which have not delegated validation to Value Objects - we will have many places where code needs to be updated. Without a source of truth, our change will be prone to human error.
Value Objects are a concept representing a value within our Domain - encapsulating rules and serving as a source of truth.
Summary
ACK/NACK protocol messages aren’t specific to CQRS systems. But if you are mapping to one, it’s useful to know ways to help you decide when to return an ACK, or when to return a NACK.
Remember to:
Communicate understanding and acceptance - not execution
Delegate validation to Value Objects
Utilise the response codes of the delivery mechanism.
“The single biggest problem in communication is the illusion that it has taken place"
— George Bernard Shaw