How to update Multiple Aggregates via single Event

A new bee here.
Axon 3.0.5 with all defaults with spring-boot.

Greatly impressed by everything just worxs. Thank you guys.

I might have came to a design choice here, so kindly enlighten me if the issue is not a technical one.

At high level I am trying to frame a question like :
"How to create different projections of same Domain Entity only using Event Sourced Aggregates "

Minimal sample for technical details:

A) TargetProfileAggregate is where my “write commands” C in CQRS is targeted, all good here.

@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileAggregate {

    @AggregateIdentifier
    private Long id;
    private List<String> msisdn = new ArrayList<>();

B) TargetProfileViewAggregate is projection (Q is CQRS) of the above aggregate handing primary “read commands by id”


@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileViewAggregate {

    @AggregateIdentifier
    private Long id;
    private List<String> msisdn = new ArrayList<>();

C) TargetProfileViewByMSISDNAggregate is another projection I want to build from same events.

@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileViewByMSISDNAggregate {

    @AggregateIdentifier
    private String msisdn;
    private List<Long> id = new ArrayList<>();

D) The issue is “TargetProfileViewByMSISDNAggregate” has @AggregateIdentifier as it has to serve commands having different @TargetAggregateIdentifier then so called “primary key used for write”

public class GetTargetProfileViewByMSISDNCommand {

    @TargetAggregateIdentifier
    @NotBlank
    String msisdn;

Full source code:
Currently “TargetProfileViewByMSISDNAggregate” does not get built on Command “GetTargetProfileViewByMSISDNCommand”, So what is the “right” way to build such aggregates?


@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileAggregate {

    @AggregateIdentifier
    private Long id;
    private String name;
    private List<String> msisdn = new ArrayList<>();

    private HashMap<Long, String> contents = new LinkedHashMap<>();

    @Autowired
    private BlobStore contentStore;

    @CommandHandler
    public TargetProfileAggregate(TargetProfileCreateCommand command) {
        log.info("Handling {}[{}]", command.getClass().getSimpleName(), command.getId());
        apply(TargetProfileCreatedEvent.builder().id(command.getId()).auditEntry(command.getAuditEntry())
                .name(command.getName()).build());
    }

    @CommandHandler
    public int on(AddMsisdnForTargetProfileCommand command) {
        log.info("Handling {}[{}]", command.getClass().getSimpleName(), command.getId());
        if (msisdn.contains(command.getMsisdn())) {
            throw new DataIntegrityViolationException("Can't associate duplicate msisdn " + command.getMsisdn());
        } else {
            apply(AddMsisdnForTargetProfileEvent.builder().id(command.getId())
                    .auditEntry(command.getAuditEntry()).msidn(command.getMsisdn()).build());
            return msisdn.size() - 1; //always gets added at last index.
        }
    }

    @CommandHandler
    public long on(AddContentForTargetProfileCommand command) throws IOException {
        log.info("Handling {}[{}]", command.getClass().getSimpleName(), command.getId());
        File file = new File(command.getFileName());
        FileCopyUtils.copy(command.getFileContent(), file);
        long fileId = contentStore.save(file);
        apply(AddContentForTargetProfileEvent.builder().id(command.getId())
                .auditEntry(command.getAuditEntry()).fileId(fileId).fileName(command.getFileName()).build());
        return fileId;
    }

    @EventSourcingHandler
    public void on(TargetProfileCreatedEvent event) {
        this.id = event.getId();
        this.name = event.getName();
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }

    @EventSourcingHandler
    public void on(AddMsisdnForTargetProfileEvent event) {
        msisdn.add(event.getMsidn());
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }

    @EventSourcingHandler
    public void on(AddContentForTargetProfileEvent event) {
        contents.put(event.getFileId(), event.getFileName());
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }
}


@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileViewAggregate {

    @AggregateIdentifier
    private Long id;
    private long version;

    private String name;
    private List<String> msisdn = new ArrayList<>();

    @CommandHandler
    public TargetProfileViewResponse on(GetTargetProfileViewCommand command) {
        log.info("Handling {}[{}]", command.getClass().getSimpleName(), command.getId());
        return TargetProfileViewResponse.builder().id(id).name(name).version(version)
                .msisdn(msisdn.toArray(new String[msisdn.size()])).build();
    }

    @EventSourcingHandler
    public void on(TargetProfileCreatedEvent event, @SequenceNumber Long version) {
        this.id = event.getId();
        this.version = version;
        this.name = event.getName();
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }

    @EventSourcingHandler
    public void on(AddMsisdnForTargetProfileEvent event) {
        msisdn.add(event.getMsidn());
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }

}


@Aggregate
@Slf4j
@NoArgsConstructor
public class TargetProfileViewByMSISDNAggregate {

    @AggregateIdentifier
    private String msisdn;
    private List<Long> id = new ArrayList<>();

    @CommandHandler
    public List<TargetProfileViewResponse> on(GetTargetProfileViewByMSISDNCommand command) {
        log.info("Handling {}[{}]", command.getClass().getSimpleName(), command);
        List<TargetProfileViewResponse> targets = new ArrayList<>();
        id.forEach(targetId -> targets.add(TargetProfileViewResponse.builder().id(targetId)
                .msisdn(new String[]{msisdn}).build()));
        return targets;
    }

    @EventSourcingHandler
    public void on(AddMsisdnForTargetProfileEvent event) {
        msisdn = event.getMsidn();
        id.add(event.getId());
        log.info("Applied  {}[{}]", event.getClass().getSimpleName(), event.getId());
    }

}

I am missing something obvious ? Completely wrong design direction :slight_smile: ? Requesting to share your opinions on how to do it using EventSourcedAggregate.

Hi Dhruv,

in fact, there is something wrong with the design ;-). Projections aren’t built with Aggregates, but using “plain” beans with @EventHandler annotated methods. Unlike Aggregates, these Event Handlers receive all events and can update a query model in the database. Aggregates are the implementation of the C part of CQRS.

Check out the webinar I did with Pivotal to see how to do this: https://www.youtube.com/watch?v=Jp-rW-XOYzA

Hope this help.
Cheers,

Allard