1

I have an invoice aggregate that I create with a lot of data:

$invoice = Invoice::create(
    InvoiceId::generate(),
    $lines, // Collection of lines
    InvoiceNumber::create('1'),
    $address, // Address value object
    Currency::EUR(),
    null,
    $discounts // Collection of discounts for lines and the invoice itself
);

So this works perfectly fine when I create a new invoice or edit an existing invoice. But what if I just want to cancel an invoice?

The CancelInvoiceCommand contains just the identifier and the handler then needs to instantiate the aggregate. But I don't have nor do I need all the data above. So can I simply do something like this?

$invoice = Invoice::createForStateChange($invoiceId);
$invoice->cancel();

The problem with that is, it will use the same aggregate object that implements Invoice::create() as well. The problem I try to understand is, that when I do that, the aggregate will be in an invalid state because it has a lot other methods as well to operate on the data that is available when create() was used but not when createForStateChange() was used.

Do I have to always create an aggregate root with all available data or can I have also another aggregate that addresses the same domain but a different process / part of it? So I would have a more simplified aggregate that just relies on having the id present?


After reading this questions accepted answer Confusion about the meaning of the word aggregate in domain driven design

The key part of the answer (at least for me) is:

Partial loading of an aggregate is broken when trying to apply a change -- how could a well designed aggregate possible validate all of its consistency rules with a subset of the data? It's certainly the case that, if you have a requirement where this makes sense, your modeling is broken somewhere.

I think I begin to understand: I always want to reconstruct the whole entity to ensure it's state is correct. So I would have to always re-create the whole entity?

floriank
  • 471
  • 2
  • 16

2 Answers2

1

Do I have to always create an aggregate root with all available data or can I have also another aggregate that addresses the same domain but a different process / part of it? So I would have a more simplified aggregate that just relies on having the id present?

It actually depends. Is there a difference between an invoice which is being prepared, i.e. lines are being added, removed,... and only at a certain point of time the invoice is finalised so that no further changes can be made to the invoice and the invoice is ready to be accounted?

If there's a clear distinction between an invoice which is being made and a finalised invoice, you might be actually looking at two bounded contexts:

  1. invoice administration,
  2. accounting.

I could imagine, in the first bounded context, your invoice model could have the following methods:

  • Create
  • AddLine
  • RemoveLine
  • Delete - completely deletes the invoice
  • Finalise - finalises the invoice and locks it for further changes (and emits a InvoiceHasBeenFinalised event

In the second bounded context, your invoice model might be simpler, having only a referencing id of an invoice from the first BD, a sum of lines (so that the amount of the invoice might be calculated) and theoretically, besides a method for construction (as a reaction to the InvoiceHasBeenFinalised event 1*), this invoice could have only a single method: Cancel.

The cancel operation would not delete the invoice but (based on the requirements from your business) would most likely create a reversal operation.


1* Of course, if your application is a monolith and not split into concepts such as micro-services, the event might not be needed at all and different models across your bounded context might be simply represented by pulling the data from the database using a different query and reconstructing the loaded domain model using two different ways.


As you can see, if there's a distinction between those two invoice concepts, yes, introducing a different model is a valid case and cancelling will definitely be simpler, since you only need to construct an object having (theoretically) three properties:

  • identifier of the original invoice which had been finalised,
  • the sum of all lines for accounting purposes,
  • the state of the invoice (so that you can not cancel an already cancelled invoice).

But what if the distinction between the contexts does not exist and you actually do need to cancel an invoice with its lines?

This is one of the reasons why loaders for domain objects are exposed as interfaces, rather than concrete implementations, and sometimes implementation of the interface from the domain might live in a completely different, DAL related package.

Because sometimes reconstructing a domain model might be quite a complicated operation (about which the domain does not care), a simple interface guards you from the complexity of the actual implementation.

Consider the simplest scenario, where you only need to load an invoice by id. An interface might look like this (language agnostic):

interface InvoiceRepository {

    findById(id: InvoiceId): Invoice?
}

for which the implementation is fairly simple, you simply query all the data, construct the invoice in the implementation class and return the object (if an invoice with the id exists).

However a new requirement might come where it's necessary to load all invoices by a user, so your interface changes to the following:

interface InvoiceRepository {

    findById(id: InvoiceId): Invoice?

    findByUserId(userId: UserId): List<Invoice>
}

From the domain point of view, the interface is still very simple, but the implementation of the logic to load the data becomes much more complicated, since searching by an id or user id needs different search criteria and once you need to construct a single invoice and the other time a list of them.

In such case it's sometimes the best to start approaching your DAL layer differently and model it e.g. like this:

  • method findInvoiceIdsByUserId,
  • method loadInvoicesByIds,

and have an implementation of the InvoiceRepository kind of like the following:

class SomeInvoiceRepositoryImpl implements InvoiceRepository {

    private InvoiceLoaders invoiceLoaders;

    SomeInvoiceRepositoryImpl(InvoiceLoaders invoiceLoaders) {
        this.invoiceLoaders = invoiceLoaders;
    }

    findById(id: InvoiceId): Invoice? {
        List<Invoice> invoiceList = this.invoiceLoaders
            .loadInvoicesByIds(id);

        return invoiceList.size() == 0 ? null : invoiceList.get(0);
    }

    findByUserId(userId: UserId): List<Invoice> {
        return this.invoiceLoaders.loadInvoicesByIds(
            this.invoiceLoaders.findInvoiceIdsByUserId(userId);
        );
    }
}

Thanks to this, the only place where construction of complicated Invoice objects lives is in the InvoiceLoaders::loadInvoicesByIds methods and you can very easily create different search methods to load the data. Yes, this introduces an extra SELECT on the database (first you need to obtain the ids and then load the entities), but from my personal experience, systems are usually garbage anyway and an extra SELECT will really not hurt you at all.

Andy
  • 10,238
  • 4
  • 25
  • 50
  • Thanks a lot for sharing your experience! :) And yes, I agree about the extra select sometimes being pragmatic is better than trying to be fancy. – floriank May 11 '19 at 11:24
0

The answer is "it depends"... What does canceling an Invoice do? What data within the aggregate is necessary to validate this process? More importantly, what happens here?

$invoice->cancel($timestamp);

$invoice->add($item, $quantity);

$invoice->markAsPaid($timestamp);

Are there other methods within your Invoice that would be affected after transitioning to a "canceled" state? You certainly don't want to end up in a situation where the valid operation of your system is dependent on your use-cases only invoking certain methods (and not invoking others).

If there are no other places in your aggregate that would be affected by some previous call to cancel (which I find hard to believe), you have discovered a new entity, InvoiceStatus, that operates independently of Invoice and can be modeled as such.

user3347715
  • 3,084
  • 11
  • 16