Can different aggregates share a UUID identifier?

Hi All,

Assume I run a webshop where I sell ebook as well as paperbacks and also offer (unpublished) books for pre-order.
Obviously an order in my webshop can contain both ebooks as well as paperbacks and pre-orders.

Now I have created a (DDD) software model for my bookstore and realised that the business rules for completing a purchase are so different for ebook/paperback/pre-order that each type will be modelled with a separate aggregate. The model has an Order aggregate with the customer, invoicing and shipping details.

Now I wonder whether it is acceptable to re-use the UUID of my Order aggregate as identifier of the ebook/paperback and per-order aggregated that is it compose of? That way I can easily identify aggregates that belong to the same order.

Q1: is this approach valid / acceptable from a DDD perspective?
Q2: would Axon support this pattern assuming I stick with the SimpleCommandBus ?

Q3 Could I make this approach compatible with the DisruptorCommandBus by prefixing the order’s UUID with a distinct prefix for each aggregate type ?
e.g.

  • EBO- for ebook aggregate

  • PPB- for paperback aggregate

  • PRE- for pre-orders aggregate

Thanks for you comments,

Benoît

Hi Benoit,

DDD-wise, it sounds a bit ‘off’ to re-use identifiers for different things. If they’re different things, they should have their own identity.

However, I’m pretty sure Axon will allow you to reuse identifiers for different aggregate types. Next to the aggregate identifier, Axon always stores the aggregate type in the event store. So as long as you use the correct Repository instance to load an aggregate, you’ll be able to reuse the identifier.
Whether you’d want that… se above :wink:

By the way, I am pretty sure the DisruptorCommandBus can deal with this too.

Cheers,

Allard

Hi Allard and Benoit,

I’m one of the maintainers of Broadway (we shortly spoke at GOTO Amsterdam last year), an DDD/CQRS/EventSourcing library for PHP.
We have been discussing this topic for quit some time and pushed a decision forward for a long time about this :slight_smile:
As Allard said, it probably is not a good idea to reuse identifiers, but sometimes it can help (if only with event querying for replaying for example).

The main reason we are looking into this again, is that we are working on using accounts in multiple applications. As we only really need an account identifier, we didn’t want to create a mapping from UUID to UUID in our aggregate, so we want to reuse the same UUID in the (two) applications. But this lead to UUID collisions (in our case an unnatural amount of UUID collisions, but that is another ‘fun’ story…). So one way of solving that for us, would be to differentiate the events also on aggregate type (we call it streamType).

But Allard, you mentioned that Axon stores the aggregate type in the event store. But we were just looking into that (before I saw this thread) and noticed that you removed the aggregate type from the eventstore last year: https://github.com/AxonFramework/AxonFramework/commit/c1f3d14ddc45b63c939ce2623657a606b8792c12
Do you remember why you removed that? We were just thinking on adding it to our library, so we are really curious why you removed it :slight_smile:

With regards,
Willem-Jan

Hi,

yes, the aggregate type disappeared from the EventStore API. However, in another commit (not yet integrated in the master branch), the aggregate type is added to the DomainEventMessage interface. This makes the aggregate type also available to compontent procesing the global stream of events.

Hope this clarifies things.

Cheers,

Allard

Morning Allard & Willem-Jan,

Thanks for your feedback.
Regards,
Benoît

Hi Allard

We have tried to reuse identifiers for different aggregate types. However, when trying to get something from the Repository, it seems as if this type is not used when calling the load(). This causes the wrong events to get loaded and the system to fail with org.axonframework.eventsourcing.IncompatibleAggregateException(Aggregate identifier must be non-null after applying an event.

Any suggestion how we can reuse aggregate identifiers?

Laura

Hi Laura,

What version of Axon are you running exactly?

Taking a look in the Axon project (version 3.0.x) suggests that that exception can only be thrown from the EventSourcedAggregate.publish() function.
The publish() function can be called on two occasion:

  1. When applying an event from within an Aggregate (more specifically, the AggregateLifecycle.apply() funcion).

  2. When event sourcing an aggregate (more specifically, the `EventSourcedAggregate.initializeState() function).

Additionally, that exception would be thrown if the identifier for an aggregate can not be found when applying an event.

A reason for Axon to not be able to find the identifier within an aggregate, is because the aggregate id parameter is not annotated with @AggregateId or @EntityId.

I might be off here, could it be that the aggregate id is not annotated?

Hope this helps.

Cheers,

Steven

HI Steven

I’m using axon 3.0.2.

Let me try to explain the situation in witch the error occurs:
I have two different aggregate, CustomerAggregate and SupplierAggregate. Both have same id’s (id’s are like “1”,“2”,…). Both have the annotation @AggregateIdentifier on their id field.

I have two command handlers, one for supplier on for customer (see below).
When there are no CustomerEvents, the CommandHandler for Supplier works fine, and the other way round (no supplier events, customer works fine). When I now have a SupplierCreatedEvent(id=“1”) and the CustomerCommandHandler receives an AddorUpdateCustomerCommand, we want to check if this customer already exists. We use the load function of the eventsource for this. When debugging, I noticed that DomainEventStream eventStream = this.eventStore.readEvents(aggregateIdentifier); which is called eventually in the doLoadWithLock method of EventSourcingRepository, returns the SupplierCreatedEvent(id=“1”). Then I get the error mentioned when EventSourcedAggregate.initialize is called.

Here’s my CustomerCommandHandler & SupplierCommandHandler

@Component
@AllArgsConstructor
@NoArgsConstructor
public class AddOrUpdateCustomerCommandHandler
{
    private static final Logger LOGGER = Logger.getLogger(AddOrUpdateCustomerCommandHandler.class);

    @Autowired
    private EventSourcingRepository<CustomerAggregate> eventSourcingRepository;

    @CommandHandler
    public void addOrUpdateCustomerCommandHandler(AddOrUpdateCustomerCommand command) throws Exception
    {
        LOGGER.info("AddOrUpdateCustomerCommand: {}", command);

        getAggregateById(command.getId())
                .orElseGet(() -> Try.ofFailable(() -> eventSourcingRepository.newInstance(() -> new CustomerAggregate( //NOSONAR
                        CreateCustomerCommand.builder()
                                .id(command.getId())
                                .name(command.getName())
                                .maxPieceWeight(command.getMaxPieceWeight())
                                .region(command.getRegion())
                                .combiningOutboundDeliveriesAllowed(command.isCombiningOutboundDeliveriesAllowed())
                                .shippingType(command.getShippingType())
                                .priority(command.getPriority())
                                .labelTypeHeadLeft(command.getLabelTypeHeadLeft())
                                .labelTypeTop(command.getLabelTypeTop())
                                .labelTypeHeadRight(command.getLabelTypeHeadRight())
                                .labelTypeExtra(command.getLabelTypeExtra())
                                .manualActions(command.getManualActions())
                                .build())))
                        .getUnchecked())
                .invoke(a ->
                {
                    a.update(UpdateCustomerCommand.builder()
                            .id(command.getId())
                            .name(command.getName())
                            .maxPieceWeight(command.getMaxPieceWeight())
                            .region(command.getRegion())
                            .combiningOutboundDeliveriesAllowed(command.isCombiningOutboundDeliveriesAllowed())
                            .shippingType(command.getShippingType())
                            .priority(command.getPriority())
                            .labelTypeHeadLeft(command.getLabelTypeHeadLeft())
                            .labelTypeTop(command.getLabelTypeTop())
                            .labelTypeHeadRight(command.getLabelTypeHeadRight())
                            .labelTypeExtra(command.getLabelTypeExtra())
                            .manualActions(command.getManualActions())
                            .build());
                    return null;
                });

    }

    private Optional<Aggregate<CustomerAggregate>> getAggregateById(ID id)
    {
        try
        {
            return Optional.of(eventSourcingRepository.load(id.toString()));
        }
        // we don't want to log this exception because it's correct behaviour
        catch (AggregateNotFoundException ex) //NOSONAR
        {
            return Optional.empty();
        }
    }
}
@Component
@AllArgsConstructor
@NoArgsConstructor
public class AddOrUpdateSupplierCommandHandler
{
    private static final Logger LOGGER = Logger.getLogger(AddOrUpdateSupplierCommandHandler.class);

    @Autowired
    private EventSourcingRepository<SupplierAggregate> eventSourcingRepository;

    @CommandHandler
    public void addOrUpdateSupplierCommandHandler(AddOrUpdateSupplierCommand command) throws Exception
    {
        LOGGER.info("AddOrUpdateSupplierCommand: {}", command);

        getAggregateById(command.getId())
                .orElseGet(() -> Try.ofFailable(() -> eventSourcingRepository.newInstance(() -> new SupplierAggregate( //NOSONAR
                        CreateSupplierCommand.builder()
                                .id(command.getId())
                                .name(command.getName())
                                .usingBarcodesWithCheckDigit(command.isUsingBarcodesWithCheckDigit())
                                .qcNeeded(command.isQcNeeded())
                                .build())))
                        .getUnchecked())
                .invoke(a ->
                {
                    a.update(UpdateSupplierCommand.builder()
                            .id(command.getId())
                            .name(command.getName())
                            .usingBarcodesWithCheckDigit(command.isUsingBarcodesWithCheckDigit())
                            .qcNeeded(command.isQcNeeded())
                            .build());
                    return null;
                });

    }

    private Optional<Aggregate<SupplierAggregate>> getAggregateById(ID id)
    {
        try
        {
            return Optional.of(eventSourcingRepository.load(id.toString()));
        }
        // we don't want to log this exception because it's correct behaviour
        catch (AggregateNotFoundException ex) //NOSONAR
        {
            return Optional.empty();
        }
    }
}

Hi Steven, Allard,

Any input on this?
It’s blocking an urgent release.

Kind regards,
Koen Verwimp

Hi Laura, Koen,

Ah I get where the issue is coming from now.
Probably the simplest solution, where you can keep the code setup you currently have and keep having identical identifiers, is by prefixing the aggregate identifier when it’s stored.
I see you’ve got an ID class, which you call the toString() function on the get the aggregate identifier for either the Customer or Supplier aggregate.
You could make the ID class abstract, and introduce a CustomerId and SupplierId class, where the toString() function add’s for example ‘customer-’ or ‘supplier-’ to the aggregate identifier.
That way you can guarantee the the DomainEventStream which is used by the EventSourcingRepository to initialize the aggregate, to not contain events for other aggregates.

Hoping this helps!

Cheers,

Steven

Hi Steven

We already analysed this solution, but by doing so the ‘artificial’ id will also be in our events. Other systems that listen to these events will have to take this into account and there will be a lot room for errors.
Do you see an other, maybe more difficult, solution?

Laura

Hi Laura,

how will that artificial ID also be in your events? Your events should contain the original CustomerId or SupplierId. It’s just how Axon uses that ID object to get to a String value that changes.

Alternatively, you could use a (hard-coded) prefix in your repository. Do so by wrapping a repository and prefix the identifier with a specific value (e.g. the aggregate name). I’m not sure if you’d also need to do anything with the returned Stream, but I don’t think so.

Hope this helps.
Cheers,

Allard

Guys we have the same problem. For me its not a good solution to have a prefix. Due the fact that @allard said its possible to use equal identifiers on differente aggregates, i hope you have another solution than prefix. Have you gone further on this?

Think that proyecting the view on an relational model , means/ restricts to have string identifiers, instead numbers, due the prefix. On example

Of course we can make id matching but that is more work

Did you have the chance to investigate any soiutiion?

Hi Fernando,

Axon doesn’t support reusing the same identifier for different aggregates. That is to say: the Event Store doesn’t support it. The identifier is used to fetch Events, and must uniquely identify an aggregate’s stream.

Also note that Axon uses the @TargetAggregateIdentifier and @AggregateIdentifier annotated methods/fields to retrieve identifiers. If those carry a prefix, the ones you use in your own event objects don’t have to.
Still, I’d be very careful when using the same identifiers for different aggregates. It seems a very fragile solution to a certain problem.

Cheers,

Allard

Hi Allard,

I am in the same situation of having to Aggregate types with the same aggregateIdentifier. In Axon 2.4.x the EventSourcingRepository fetches only the events for a given type and aggregateIdentifier, but in 3.x the
EventStream is only on aggregateIdentifier as already said before. But why can’t that stream be filtered on type. Because the EventSourcingRepository knows which type it is trying to load and the DomainEventMessages contain
type information this could be done here. By excluding DomainEventMessages from the other type the construction of the Aggegate will not fail with Aggregate identifier must be non-null after applying an event

I added the lines below after the line reading the stream and this seems to work.

List<? extends DomainEventMessage<?>> newList = eventStream.asStream()
.filter(e -> e.getType().equals(aggregateModel().type()))
.collect(Collectors.toList());
eventStream = DomainEventStream.of(newList);

Can this filtering be added to the EventSourcingRepository?

Greetz,
André

Hi Andre,

For feature requests or issues, I’d like to refer to the Axon Framework GitHub page.

Apart from that, I assume that when Allard upgraded the framework from 2 to 3, the conscious decision was make to remove the Aggregate Type from that query.
So although adding such a filter isn’t a difficult request, I assume it might be declined because of some fair reasoning.

I am however not a 100% sure why that decision was made…so sorry for that.

Like I suggested on top, let’s start an issue for this on the GitHub page.
That would be the ideal grounds to discuss such an addition and track the progress.

Cheers,
Steven

Hi Steven,

I already did. I opened issue 587 and also added a pull-request for it.

Greetz,
André

Hi Andre,

Ah, that’s a reason why I should first check the GitHub issues list prior to answering the user group questions…
Thanks for created the issue, we’ll discuss it here internally!

Cheers,
Steven