0

I am totally inexperienced in modelling or DDD approach. Below description could point out to my lack of knowledge about how entities, aggregates or microservices work.

Background

So, I have a domain for invoices, where an Invoice object is an Aggregate Root. I also have a domain with person's billing or shipping data, called Person.

My Invoice model is being created by use of simple event sourcing. Let's say that i have an InvoiceCreated event, with invoice ID, issuing person ID, status and creation date:

class InvoiceCreated {
  private string id;
  private string issuerId;
  private string status;
  private Date dateCreated;

However, my Invoice entity does not contain the ID of issuer, but the whole Value:

class Invoice {
  private string id;
  //...
  private Issuer issuer;

The Issuer class is just a VO created from another domain Entity. Both domains communicate with each other by REST API.

The problem

I am recreating an Invoice entity from events, which has only ID of objects from another domains. I do not have direct access to this data from my Invoice domain. My aggregate root has apply method which applies and records events. So, in order to correctly apply mentioned event, I need to have data of Person that is creating (issuing) this invoice.

Possible solutions

I am thinking about supplying my aggregate root with some kind of service/API client that would retrieve this data from the outside. It would make an API call for this data, and then store this in Redis for the time of replaying events, and then invalidate the cache.

Main con of this approach is immutability of my event - in case of Person's model change all I have to do is change in Invoice's Person value - no need to alter event at all, no need to create a new version of event.

But I am afraid about correctness of this approach. The idea seems pretty good and right for me, but implementation confuses me a bit. I am thinking about breaking the SRP rule with this approach and making a mess in code.

What do you think? Maybe it's my model which is wrong? Or is this right thing to do in that case? I am not using any event handlers, or event bus - it is a very simple case and I thought that it is not a bad thing to apply events in aggregate itself and uses them as event store entries as well.

  • Did you forget this question? – plalx Dec 13 '19 at 00:02
  • I just needed some time to think about it all. I'm still anxious when it comes to modeling and I think that I'm doing many things wrong. I didn't forget about it, I am just still thinking about the solution. – K.Baczkiewicz Dec 18 '19 at 21:26

3 Answers3

2

Is it OK to inject service/API client to aggregate root?

Usually, the domain model is in-memory data structures and methods to manipulate in memory data structures. Trying to reach across a process/application/network boundary is weird. "Get data from somewhere else" is the sort of thing that you would normally do from the application layer, not from the domain layer.

It's also strange for an entity in your domain model to include a reference to another entity that is not part of the same aggregate.

It's very odd for an "event sourced" entity to require data that isn't in the events. That sometimes happens because we want to store things separately (for instance, an entity needs data with "right-to-be-forgotten" policies), but you would normally handle that as part of your persistence requirement.

For data that lives "somewhere else", there are two common approaches. One is to have a copy of the data cached within your domain entity itself (so not "the" issuer, but "my stale copy of" the issuer), the other is to have that data passed to the model by the application layer.

invoice = Invoice.fromEvents(...)
issuerId = invoice.issuerId()
issuerData = findTheIssuer(issuerId)
invoice.onIssuer(issuerid, issuerData)
// ...

One useful litmus test: you should be able to run all of your domain logic in a unit test with no calls out to shared mutable state. If you can't do that, it strongly suggests that your boundaries are drawn incorrectly, and you should re-evaluate your design.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91
0

I would say that it is a bad idea to inject some external service inside an aggregate. In my opinion an aggregate should correspond to an event stream and should be self-consistent. Based on current events in the event stream and data contained in a command it should make up a decision whether to produce another event or not.

Next, what is the domain of invoices? It seems that invoice is incorrect aggregate boundary here. Maybe that could be an order, a contract or something like this. That defines a business value and a business boundary. For such an aggregate an invoice could be just one of reports i.e. read models. It would incorporate data from other bounded contexts as well. Like customer name and address, which does not belong to initial context.

I can't remember the link from the top of my head, by Udi Dahan has some inspiring talk about design of Amazon cart which gets data from multiple contexts. So, you case needs more thought as well. When you consider an invoice to be a read model - then it is perfectly ok to have multiple data sources to build it. But when you manage data consistency within you boundaries - then prefer to be self-consistent.

What you recreate from events of a single aggregate is effectively a snapshot of the aggregate that is needed for decision-making. And, yes, an event must be immutable. Because updating an event is like fixing past time. It may lead to a state which could have never existed.

iTollu
  • 999
  • 13
  • 20
0

I am recreating an Invoice entity from events, which has only ID of objects from another domains. I do not have direct access to this data from my Invoice domain.

Is there any invariants you're attempting to protect that requires this external data? If not then you're incorrectly trying to leverage your Invoice aggregate as a projection for facilitating queries. Event-sourced ARs' state should only represent data needed for command execution, not queries.

For instance, the Invoice AR would most likely not be involved in a use case that generates a printable representation of an Invoice.

So, I have a domain for invoices, where an Invoice object is an Aggregate Root. I also have a domain with person's billing or shipping data, called Person.

I'm not so familiar with e-commerce domains, but the contexts segregation looks a bit odd to me. I'd most likely expect the billing address to be part of the Billing/Invoicing context. The Person/Customer may have a default billing address, but this data should most likely get copied over for every Order and therefore Invoice. When a Customer change it's default billing address, it should probably not reflect on past orders/invoices and he'd be asked what to do for ongoing ones. What I'm trying to say here is that perhaps the billing address is a VO that should be part of the InvoiceCreated event.

Finally, if your AR must access data which is not part of it's own consistency boundary then you may:

  1. Cache a stale copy of the data in other ARs (could synchronize through eventual consistency). Note that in the above example, the copied billing address is probably necessary as it belongs to the specific Invoice instance, it's not to facilitate external data access.

  2. Pass the data through method arguments. The data will be fetched externally (e.g. in an application service) and then passed to the AR's use case method.

  3. Pass a data provider service through method arguments. This is similar to #2, except the AR will interact with the service directly to gather the data. This approach can be useful if the logic for fetching the data is complex and depends on the AR's state. Note that the service dependency would be defined on an interface, not a concrete implementation.

plalx
  • 42,889
  • 6
  • 74
  • 90
  • I think I would go with solution number 1 - just create a copy of the data I need to push to my Invoice entity. I think this is an easiest approach. – K.Baczkiewicz Dec 18 '19 at 21:30
  • @K.Baczkiewicz It's not about what's easiest in this case, but what's correct. Considering the billing address shouldn't change automatically for past invoices just b/c a customer changed their current default billing address, you MUST copy over the data. – plalx Dec 19 '19 at 03:38
  • I bear that in mind. I get a copy of this data stored in events (and, therefore, in entity of invoice itself). I simply mean that it is the easiest way of dealing with my problem, cause now the source of this data is irrevelant as I parse them to my models. – K.Baczkiewicz Dec 19 '19 at 11:49
  • @K.Baczkiewicz I dont think it's a good idea to consume read models data into ARs. – plalx Dec 20 '19 at 13:16