I’m trying to follow the Clean Architecture and CQRS within an application that, let’s say, create a product.
To achieve this, the application has 3 adapters, one for reading a file, another for consuming a Kafka topic and another for receiving REST API requests. These are the three ways that is possible to create a product, since the producers belong to distant teams and it’s not up to me to decide which technology they will use.
The first question: is there a problem with having only one command handler for these 3 adapters? Obviously, they will map each request/event into a common command and then dispatch it.
The second question: where will be the validation? I’m currently doing it on a separate validator class that is called from the command handler, but I’m facing the problem that each request has it’s own fields and conventions, i.e. on Kafka topic the product name field is “product_name” and on CSV file is “ProductName”, and the error message will be based on the command attributes (“the productName is invalid”) since I’m validating only the command object. Because of all of these differences it’s being hard to standardize callback/responses to each product producer.
How would you handle this situation?
The first question: is there a problem with having only one command handler for these 3 adapters? Obviously, they will map each request/event into a common command and then dispatch it.
Depends on how many command handlers you need. But if you think you need three because you have three adapters then the adapters haven’t done their job.
After the adapters have done their work your model of the command shouldn’t even be aware of which sent the request. Your adapters job is to turn each different request type into one type.
The second question: where will be the validation? I’m currently doing it on a separate validator class that is called from the command handler, but I’m facing the problem that each request has it’s own fields and conventions, i.e. on Kafka topic the product name field is “product_name” and on CSV file is “ProductName”, and the error message will be based on the command attributes (“the productName is invalid”) since I’m validating only the command object. Because of all of these differences it’s being hard to standardize callback/responses to each product producer.
This seems like a symptom of the adapters not doing what I talked about them doing in your first question.
Validation is a good way to specify your needs. Each adapter needs the request to conform to it’s needs. So sure. But the command handler also has needs.
It comes down to trust. If the command handler trusts the adapters to conform to its needs then validation is much less important. Not sure you should trust the things making these requests though. That sounds like stuff that will be off living an independent life.
5
You need to untangle this and separate the steps more than you currently are. In order to make this make sense, let me pick at the threads:
I’m facing the problem that each request has it’s own fields and conventions, i.e. on Kafka topic the product name field is “product_name” and on CSV file is “ProductName”
That’s fine, but we need to distinguish between the command and the request here. The command should be the same for all three entrypoints. Each entrypoint merely maps its incoming request (whatever) its structure might be) to that one command.
and the error message will be based on the command attributes (“the productName is invalid”) since I’m validating only the command object.
We strike on a contradiction here. You are designing your validation in the (one reusable) command, but you expect it to give you a response that is custom-tailored to a specific entrypoint (where many different entrypoints exist).
This is the part you need to untangle.
Going back to the theory behind it all, both the purpose and the consequence of having a reusable command that multiple consumers rely on is that you design it to work agnostic of the consumer of the command.
This is good, because it helps you avoid pointless repetition, but it comes at the cost of actually having to remove any and all dependencies on the consumer. That field name is such a dependency.
There are several ways you can tackle this cleanly. I don’t like all of them, but I’ll list them in the order that they come to mind, and I’ll comment on my opinion of them. At the end of the day, you have to decide what makes the most sense for your use case. All I can do is to urge you to not cut corners here and make you decision on what is best for you, not what is easiest.
- The consumer can tell the command what it’s field name is.
I don’t like it, but it’s an option. The reason I don’t like it is that this would require the consumer to pre-emptively create a dictionary of field names for the command to use, plus there are edge cases where it’s not always straightforward to map one request field to a command field (e.g. if your request takes in a separate first and last name, but the command takes in a single merged name field – which field broke?)
- [variation on 1] The returned error message uses a substitutable placeholder for the field name. It’s the request handler’s job to swap this out for the correct name.
I don’t like this either. It requires way too much error message handling, plus the error message is now worse if you don’t run the substitution logic.
- Structured error codes
Doesn’t have to be a code, but what I’m referring to here is that the command doesn’t just return a string message, it returns a way for you to identify which kind of (known) error was encountered. This is commonly done using an error code.
This way, you consumer is able to read the code and decide if it would prefer to return its own error message instead.
For example, the command could return both a default message (e.g. “The product name is invalid”) and an error code (“PRODUCT_NAME_INVALID”). The request handler can then read this code, and decide to return the default message or a custom one (e.g. “product_name is invalid”).
I like this better, because it separates the responsibilities well: the default error describes the core issue, the request handler focuses on request-handler-specific knowledge (the field name). I also like the opt-in nature of the work. If a consumer does not care about rewriting the error message, it can just not do so and use the default message.
- Stop caring about the specific field name.
As I pointed out in 1, it’s not always going to be possible to easily blame one request field for a particular error.
A great example here is a login error: did you provide the wrong password, or did you provide the wrong username? Both would lead to the same issue (you did not supply the correct password for this username); but it would be impossible to point at a field and blame it.
Yes, in some cases it is possible to do so. For simple structures, probably in a lot of cases.
But the design of a system, in my opinion, should not hinge on a coincidence and then suffer all of its edge cases. It should be designed to work in the way that encompasses what it needs to do.
I would stick with the command-centric error messages, rather than getting distracted about the field names. These field names are inherently a developer concern (not an end user concern, which is an important distinction here). This developer would already be aware of the request handler (i.e. the one they’re using which is returning an error), which means that they would already be trivially able to understand which command-based error would relate to which request-based field.
If they aren’t able to understand that, then I would suggest that you spend your effort in simplifying your interface and model, rather than coming up with a contrived reusable custom error message format.
3