1

I tried to combine two rules.

  1. Don't fall to primitive obsession (e.g. EMailAddress should be a value object in domain objects and not a string).
  2. Don't use value objects in commands and events.1, 2, 3

If commands and events should not contain value objects (for obvious and often discussed reason) they contain something as " EMailAddress" as string. In the aggregate it should be handled as an EMailAddress value object with it's own constraints and checking.

So far, so good.

But where does the conversion happen? Do we call a "void DoIt(string eMailAddress)" method on the aggregate root and one of its first actions is to convert the string into an EMail value object? Or does the CommandHandler do the conversion and the aggregate root method looks like "void DoIt(EMailAddress eMailAddress)"? Solution 2. seems more elegant, because it holds the aggregate root free of primitives and conversions. But what about the events and even more interesting raising the event?

Example AggregateRoot Method with solution 2:

    public void ChangeEMailAddress(EMailAddress eMailAddress )
    {
         //Do some interesting stuff without changing state 
        ApplyChange(new  EMailAddressChanged(???));
    }

The ??? is the problem. If the event should not contain value objects the EMailAdress value object must be converted back into a string. Is a (second) constructor of the event (that takes the value object) allowed to do it, to keep the aggregate free of the conversion?

And what about the handling of the event in the aggregate root? In Simple CQRS Style it would be like this:

    private void Apply(EMailAddressChanged e)
    {
        this.eMailAddress = new EMailAddress(e.eMailAddress);
    }

So the conversion is again in the aggregate. We could also use EventHandlers here that do the conversion and then the method will look like:

    private void  EMailAddressChanged(EMailAddress eMailAddress)
    {
        this.eMailAddress =  eMailAddress ;
    }

(Yes I know thats simple CRUD stuff that maybe should not done the whole CQRS/ES way at all, assume that some logic is involved)

But there is one drawback here. In either solution, the conversion could fail. But an event should never fail. If I change the validation in the constructor of EMailAddress it is possible (if I use event sourcing) that older events can no longer be processed. So should I have 2 constructors/static create methods on my value objects? One with and one without validation?

So:

  1. Where do you do the conversion from primitive to value object and back in the 3 cases Handling of the Command, Raising of the Event, Handling of the event?
  2. How do you deal with value objects in event sourcing to ensure its constructor validation do not fail for older events?

I found Where to convert primitive types in meaningful types in Clean Architecture / Onion Architecture and it's partly related, but does not answer my question.

EDIT:

To make my question more clear, assume the following Structure:

  1. A command comes accross the wire. It should be a stable contract that should not change if I change my domain (write) model. So it should not contain any value objects from it. So it contains kind of dtos instead of value objects or the value object is mapped to multiple fields with primitive types.
  2. The command is taken by a command handler in the application layer. This loads the aggregate from a repository, dispatch the command to the aggregate root and then stores the aggregate in the repository. When dispatching the command to the aggregate root, a conversion has to be done from primitive types to value objects cause I don't want that conversion to be done in the aggregate root. It's no domain stuff.
  3. The aggregate root makes some validations (is the operation valid in the current state), calculations, calls DateTime.Now or whatsoever. Then it fires an event. THIS IS MY MAIN PROBLEM.
  4. An event handler takes the event and applies it to the aggregate root. Here again the primitive types/dtos must be converted to value objects cause I do not want to do that in the aggregate root.
  5. The event is published and stored and should not contain any value objects from the domain because of the same reasons as the commands. They are a contract for e.g. the projections of the read model or other event consumers.

The conversion of the commands and events primitive types to value objects could be done in the handlers. But what about the CREATION of the event in the aggregate root?

I see 2 possible solutions:

  1. The creation (and publishing) of the event is done by some kind of injected abstract factory that takes value objects and converts them into the events.
  2. The aggregate root creates and publishes events with value objects and this events are then converted to the domain value object free events outside of the aggregate.

While 1. makes you implement a complex factory, 2. implies implementing all events in 2 versions plus a converter between them.

If we go with 2. I had the additional idea to make a whole second layer of (domain)commands(and handlers) and (domain)events(and handlers). A layer of commands and events that contain domain value objects and are dispatched/created to/by the aggregate root and in the applicaionlayer converted to "contract" commands/events without any (write) model value objects.

Is there any other solution/simplification? Which way would you go?

9
  • 1
    "If commands and events should not contain value objects (for obvious and often discussed reason)". Would you mind linking to some of these discussions? They aren't as easily searchable as you might think. Oct 21 '20 at 11:04
  • 1
  • 1
    Thank you. Allow me to show you a way to edit that into the question. Oct 21 '20 at 11:32
  • Thank you for the edit.
    – Hothie
    Oct 21 '20 at 12:05
  • "for obvious and often discussed reason" - the issue is not that an event contains a value object (that in itself is fine, a value object is just something with value semantics). The issue revolves about events containing value objects from an inner domain layer. The problem is that, as you develop and maintain the software, you'll be changing the domain - you'll add or remove fields, restructure things, etc. Events and remote commands, on the other hand, being a form of an API, need to remain comparatively stable, so it's not ideal if they contain those value objects. Oct 21 '20 at 16:59
1

Let's approach this topic a big broader.

Why do we want to avoid using primitive types? Because we want to assign as much semantics to the value as we are aware of. This means, we want to attach the "context" of the value to the value itself. Including: meaning, technological constraints, behavior, etc.

It is important to note that this is technically not necessary for the software to work, just as static typing (types) are not necessary either. It's just that the less precise we describe something the more difficult it is for people to understand, use, and easier to abuse. The less precise we describe something, the more knowledge the developer has to remember implicitly (i.e how can I use this, where can I pass it, what format it is, etc.)

It follows, that the best would be to attach the type to the value as soon as you are aware of what that value is, and unpack only if and when that semantics no longer applies.

For example, when parsing a json input, parsing the "email" would mean that I become aware that that is an email address, so I would need to create a type for that. I should "unpack" that to something that no longer has the "email address" semantics. For example another json message, html, etc. This means I should never ever have "string emailAddress" anywhere in my code, ever. Because that would imply that I know what it is, but I refuse to explicitly attach this semantics to it.

It follows that any architecture or design pattern that leads you to write "string emailAddress" somewhere is not doing a good job.

Sidenote: Primitive types only need this if there is some semantics attached to it. If there is no specific behavior that applies to it, there is no reason to create a type for it either.

0

I don't think there's a good, authoritative answer for this -- the literature is kind of a mess.

My suggestion is to start by trying to understand the problem from a domain modeling perspective alone; after that, take those ideas and try to see how the event sourcing patterns fit.

Very broad picture: what we are trying to do is express the dynamics of our domain using the domain's own vocabulary, but the tools that we are using to do that are general purpose.

The information coming to us over the network is, at one level of detail, just a sequence of bytes. We take that sequence of bytes, and start parsing it into more specialized forms (is it an HTTP request? does it have a UTF-8 document in the payload? Does that document conform to the json standard? do the fields in that json document conform to our more specialized schema, and so on). And eventually we can say "this is a Refund request for Order 12345".

To understand the dynamics of that request - we are generally going to need to consult other information about Order 12345 that we have somewhere else. So our information, in its specialized form, needs to be turned into some more general purpose thing that is, in effect, a query for the data. We send that off to our data store, and get a general purpose message back, that also has to run through a parser obstacle course until we have Order.

Once we have all of the information in its domain representation, we can now apply the domain rules to it. At the top level, this is expressed in the vocabulary of the domain. But what we are really doing is manipulating information, which is captured in data structures. It's not turtles all the way down, but again a transition from domain concepts to general purpose data structures and primitives.

These transitions are normally handled by parsers. (Note: this is one of the places where I think the literature is weak, as it tends to emphasize checking in constructors -- which isn't a bad idea, but it is a different idea, it serves a different purpose).

So, where do we parse? The usual answer is that we want to parse out near the boundaries, where things aren't object oriented anyway. After all, parsing isn't a domain concern - it's a compensation for the fact that we're changing the information we need into different shapes.

Thus, the general rule is that you are parsing information into its modeled form before the dynamics are invoked.


the conversion could fail.

Yes, it could, but you need to be really careful about what you mean by that. Being strict about untrusted information is a good idea. If you want to say that your automated happy path should only permit the introduction of new email addresses that are RFC 822 compliant addr-spec, that's probably fine. But if the admin updates your trusted store to deliberately introduce a non-compliant address, your automation probably shouldn't be vetoing that decision. But if the data and the checksum don't match, you might want to put a full stop on the entire world until things get fixed, because the admin shouldn't ever do that on purpose, and so on.


Event sourcing... well, event sourcing adds a second layer of bad literature on top of the mess we already have.

RefundApproved is a domain concept, and should therefore be modeled in your domain. Because the decision to approve the refund is coming out of your domain dynamics, the interface to create that event should be expressed in the domain language, not in general purpose language. When you query RefundApproved to get an Amount, you are getting a domain value, not a primitive.

Because domain values are interchangeable (nobody cares which 10 USD the refund amount is), you've got some freedom to decide whether RefundApproved is a facade in front of a general purpose data structure, or if it is a facade in front of a graph of domain values.

When you parse the event, to turn it into a representation that you can store, you will eventually need to get to some general purpose representation of the information.

1
  • Sounds plausible. But I'm still not exactly sure at which point I will do the conversions. I will edit my question to make it more clear. Maybe we can than map your answer to it.
    – Hothie
    Oct 22 '20 at 5:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.