Saving AggregateMember to database on Query/Projection Side

Hi,

first of all I am completely new on developing cqrs with java and axon.

Introduction:

I have a bounded context named carpark. When a admin create a new carpark, he can choose auto generated parking lots or does special configurations how to create the lots.

In my opinion its not the right place to use CQRS, because a carpark will be created one time and have in rare cases some changes. Nevertheless I decided to use it, because the lots should be later reserved by the reservation bounded context, which its perfectly suited for the cqrs pattern.

Conditions:

The lots could not been created standalone and belongs always to a carpark. So lots are an AggregateMember.

The command AddCarPark get only the amount of how many lots should be generated (more complex example is out of scope for the moment).

What if have done:

public class CarParkAggregate {

    @AggregateIdentifier
    CarParkId carParkId;

    @AggregateMember
    Map<CarParkId, Lot> lots;

    ...
    @EventSourcingHandler
    public void on(CarParkAdded e) {
        log.debug("APPLYING {}", e);
        this.initCarparkEvent(e);
        for(int i = 1; i < e.getLotAmount(); i++) {
            Lot lot = new Lot(
                    new LotId(),
                    e.getCarParkId(),
                    i
            );
            lots.put(e.getCarParkId(), lot);
        }
    }
    ...
}

The AggregateMember class:

@RequiredArgsConstructor
@AllArgsConstructor
public class Lot {

    @NonNull
    @EntityId
    LotId lotId;

    @NonNull CarParkId carParkId;
    ReservationId reservationId;

    @NonNull Integer lotNumber;

}

The CarParkProjection should save the new carpark to the database, which is getting the event.

public class CarParkProjection {    
    ...
    @EventHandler
    public void on(CarParkAdded e) {
        log.debug("PROJECTION {}", e);
        
        CarParkAddress address = mapper.map(e.getAddress(), CarParkAddress.class);

        CarParkView carpark = new CarParkView(
                e.getCarParkId(),
                e.getIataCode(),
                e.getName(),
                e.getDescription(),
                address,
                e.getSupportEmail(),
                e.getSupportPhone(),
                e.getTax(),
                e.getState()
        );

        carParkService.addCarpark(carpark);
    }
    ...
}

But the lots are not saved, because i haven’t this information on the event. I don’t know how I can solve this!

Thanks for advice!

Hi @BKlemm, and welcome to our forum!

But the lots are not saved, because i haven’t this information on the event. I don’t know how I can solve this!

Honestly, I’d not be able to answer “where” to get the data from either!
Simply because I don’t know anything about your domain. :wink:

At the moment, I have three thoughts pop-up that might help:

  1. There’s an earlier/later event that can fill in the additional information at an earlier/later stage in the process, or
  2. You need to dispatch a query to enrich your query model when handling the CarParkAdded event, or
  3. You add the additional data to the event upon dispatching

Which of these three is best suited isn’t entirely clear to me.
Mainly because I don’t know where the missing information should come from, to begin with.

It would thus be beneficial if you could state where you’d expect that additional information to live.
So, if you could answer any of the following, that would help everybody to help you further with this, @BKlemm:

Does the user provide it?
Should another service provide it?
Is it already in your system at a different point in time?

Hi Björn,

first of all, I don’t know anything about your domain either (as Steven mentioned). However, I find a few bits of your code a little strange. I might be completely wrong here but I will try anyway.

(1) Aggregates

So, an Aggregate is a class that has its state stored as an ordered series of events (hence event-sourcing). Axon persists your events automatically, you don’t need to worry about these.

You can apply events to your aggregate if you need some piece of information from an event in subsequent command handlers. If you don’t need that information for decision making login in command handler, don’t store in aggregate class fields. It is not necessary. You will use that information in other event handlers during your projections.

There is one exception and that is the first event used to create the aggregate. This event needs to hold aggregate’s, CarParkId in your case, and in the event sourcing handler, you need to assign that id value to your carParkId field annotated with @AggregateIdentifier as Axon Framework reads that exact field after processing the first event when creating a new instance to return the id value all the way through the command gateway.

(2) Aggregate Members

A field annotated with @AggregateMember is a cool feature of Axon Framework that allows you to route commands to a different class than your actual aggregate. Your class Lot seems to be just a plain POJO, it does not contain any command handlers nor event sourcing handlers. Therefore marking your map of lots as aggregate member does not make any sense to me.

(3) The Map<CarParkId, Lot> thing

Why do you need a map? And looking at your code above, I don’t see how this would work otherwise than just having a map with one entry only. Let me explain…

So the aggregate CarParkAggregate has a unique CarParkId. When handling CarParkAdded events, all these event will have the same CarParkId, which you use as a key to your map of lots. If this is true, then with each CarParkAdded event you overwrite the current Lot with a new one.

Also, why do you need to duplicate the CarParkId anyway? You already have it in the Lot class, why do you need to use it as a key in a map? A simple List<Lot> lots could be enough.

But again as I mentioned in (2) if you don’t handle commands and/or events in the Lot class, consider removing the @AggregateMember annotation. Also, if you don’t use the map of lots to do some validations and decision making in any CarParkAggregate class command handlers, then remove the field completely.

(4) Design Thoughts

Knowing next to nothing about your domain, just trying to understand it from your code, I would …

Make a command to add a new car park to my system.

@Value
class AddCarPark {
  @TargetAggregateIdentifier
  CarParkId id;
  String iataCode;
  String name;
  String description;
  String address;
  String supportEmail;
  String supportPhone;
  String tax;
  String state;
  int lotAmount;
}

Describe the changes in my system as events

@Value
class CarParkAdded {
  CarParkId id;
  String iataCode;
  String name;
  String description;
  String address;
  String supportEmail;
  String supportPhone;
  String tax;
  String state;
  int lotAmount;
}

@Value
class LotAdded {
  CarParkId id;
  Integer lotNumber;
}

Define an aggregate for a car park itself

@Aggregate
class CarPark {
  @AggregateIdentifier
  private CarParkId id;

  @CommandHandler
  public CarPark(AddCarPark command) {
    // signal that a car park is added to the system
    AggregateLifecycle.apply(new CarParkAdded(command.getId(), ...));

    // signal that each lot (up to lot amount) is also added to this car park
    for (int i = 1; i <= command.getLotAmount(); i++) {
      AggregateLifecycle.apply(new LotAdded(command.getId(), i));
    }
  }

  @EventSourcingHandler
  public void on(CarParkAdded event) {
    this.id = event.getId();
  }
}

Finally use those two events in my projections

public class CarParkProjection {    
    @EventHandler
    public void on(CarParkAdded e) {
        CarParkAddress address = mapper.map(e.getAddress(), CarParkAddress.class);

        CarParkView carpark = new CarParkView(
                e.getCarParkId(),
                e.getIataCode(),
                e.getName(),
                e.getDescription(),
                address,
                e.getSupportEmail(),
                e.getSupportPhone(),
                e.getTax(),
                e.getState()
        );

        carParkService.addCarpark(carpark);
    }

    @EventHandler
    public void on(LotAdded e) {
        carParkService.addLotToPark(new LotView(e.getId(), e.getLotNumber()));
    }
}

Something simple like that will of course get more funny when you add lot registration and stuff like that.

HTH,
David

Hi David

thank you for making this clearer.

I have solved it by putting the lotAmount to the event (CarParkAdded) and generated the Lots (LotView) on Projection side by doing without an AggregateMember.
Because of misunderstood by my self what is an aggregate in axon and the interpretation of an AggregateMember. Your answer helps me a lot, to make some things clearer.

A question left: actual I am saving the read-model(s) in my mariadb database, although I am using event-sourcing. Should I renounce it?

Thanks to you and @Steven_van_Beelen ,
Björn

1 Like

From Axon’s (and AxonIQ’s, actually) perspective, Event Sourcing’s heavyweight lies with the Command Model. Event Sourcing dictates that a model is recreated based on the events it has published.

Since your query models / read models / projections (take your pick how you want to call them) don’t publish any events, they aren’t event-sourced either.
Those models are updated through “regular” Event Streaming instead.

So to answer your question: No, you don’t have to renounce them.
It is perfectly fine to have your read-model(s) in a MariaDB (or any storage solution of your choice).
That is the intent of Command-Query Responsibility Separation, actually!

Good to hear you’ve found a solution that works for you, @BKlemm.
I do hope the above helps you further, too.
And, be sure to start new threads if you’re looking for more guidance.
Everybody here is glad to help you out.

1 Like

Hi Björn,

As Steven pointed out. Keep your read models in maria db. It is just a projection based on events. That is the beauty of CQRS - the separation of commands part and queries part.

David

1 Like