CQRS / Event Sourcing

Du concept au code

Lyon Craft 2025

Aurélien Mino

Cédric Chateau

Cédric

Senior Dev @ malt logo

❤️ CQRS / ES

💔 Acronymes

Cédric

Senior Dev @ malt logo

❤️ CQRS / Event Sourcing

💔 Acronymes (non expliqués)

Aurélien

Lead Tech @ sully logo

❤️ Open-source

Pourquoi ce sujet ?

  • Patterns méconnus / peu utilisés

Pourquoi ce sujet ?

  • Patterns méconnus / peu utilisés

  • Cédric adore CQRS / ES 😄

Pourquoi ce sujet ?

  • Patterns méconnus / peu utilisés

  • Cédric adore CQRS / ES 😄

  • Partager à travers un exemple concret

Demo time

Event storming / Event modeling

Event modeling Gift Cards

event modeling

Design de l’agrégat

State sourcing / CRUD

state1
state2
state3

State sourcing - Avantages

  • Facile à comprendre

  • Facile à lire

  • On fait tout le temps comme ça

State sourcing - Inconvénients

  • Perte d’information

  • Pas de traçabilité

  • Obligation d’anticiper les besoins futurs

Event sourcing

 

Event sourcing

Début

Event sourcing

Début …​ événement …​ événement

event0
event placeholder
event placeholder
event placeholder
event0
event1
event placeholder
event placeholder
event0
event1
event2
event3

Event Sourcing

event0
event1
event2
event3

C’est parti !

  • State sourcing

  • Event sourcing

Code

🔍

→ Les événements

L’agrégat

L’application service

Interface GiftCardEvent

@DomainEvent
public sealed interface GiftCardEvent
  permits GiftCardDeclared, PaidAmount, GiftCardExhausted {

  SequenceId sequenceId();

  Barcode barcode();
}

GiftCardDeclared

@DomainEvent
public record GiftCardDeclared(
  Barcode barcode,
  SequenceId sequenceId,
  Amount amount,
  ShoppingStore shoppingStore
)
  implements GiftCardEvent {}

PaidAmount

@DomainEvent
public record PaidAmount(
  Barcode barcode,
  SequenceId sequenceId,
  Amount amount,
  LocalDate on
)
  implements GiftCardEvent {}

GiftCardExhausted

@DomainEvent
public record GiftCardExhausted(Barcode barcode, SequenceId sequenceId) implements GiftCardEvent {}

Code

🔍

Les événements

→ L’agrégat

L’application service

GiftCard.declare()

@AggregateRoot
public class GiftCard {

  public GiftCard(GiftCardHistory history) {
    // ...
  }

  public static GiftCardEvent declare(GiftCardDeclaration giftCardDeclaration) {
    return new GiftCardDeclared(
      giftCardDeclaration.barcode(),
      SequenceId.INITIAL,
      giftCardDeclaration.amount(),
      giftCardDeclaration.shoppingStore()
    );
  }
}

giftCard.pay()

@AggregateRoot
public class GiftCard {

  public GiftCard(GiftCardHistory history) {
    // ...
  }

  public List<GiftCardEvent> pay(Payment payment) {
    PaidAmount paidAmount = new PaidAmount(
      this.barcode(),
      this.nextSequenceId(),
      payment.amount(),
      payment.date()
    );

    return List.of(paidAmount);
  }
}

giftCard.pay()

@AggregateRoot
public class GiftCard {

  public GiftCard(GiftCardHistory history) {
    // ...
  }

  public List<GiftCardEvent> pay(Payment payment) {
    PaidAmount paidAmount = new PaidAmount(
      // ...
    );

    if (this.hasExactBalance(payment.amount())) {
      return List.of(
        paidAmount,
        new GiftCardExhausted(this.barcode(), paidAmount.sequenceId().next())
      );
    }

    return List.of(paidAmount);
  }
}

Code

🔍

Les événements

L’agrégat

→ L’application service

GiftCardApplicationService.declare()

@Service
public class GiftCardApplicationService {
  private final GiftCardEventStore eventStore;
  private final EventPublisher<GiftCardEvent> eventPublisher;

  @Transactional
  public void declare(GiftCardDeclaration giftCardDeclaration) {

    GiftCardEvent event = GiftCard.declare(giftCardDeclaration);
    eventStore.save(event);
    eventPublisher.publish(event);
  }
}

GiftCardApplicationService.pay()

@Service
public class GiftCardApplicationService {
  private final GiftCardEventStore eventStore;
  private final EventPublisher<GiftCardEvent> eventPublisher;

  @Transactional
  public void pay(Barcode barcode, Payment payment) {
    GiftCard giftCard = new GiftCard(eventStore.getHistory(barcode));
    var events = giftCard.pay(payment);
    eventStore.save(events);
    events.forEach(eventPublisher::publish);
  }
}

Comment afficher la liste des cartes cadeaux ?

  • Paginée 🤔

  • Triée 😮

  • Filtrée 😱

Une projection !

projection
fold00
fold01
fold02
fold03
fold04

Code

🔍

Query Model

@QueryModel
public record GiftCardCurrentState(
  Barcode barcode,
  Amount remainingAmount,
  ShoppingStore shoppingStore,
  boolean exhausted
) {

Event publisher

@Service
public class GiftCardApplicationService {

  public GiftCardApplicationService(
    GiftCardEventStore eventStore,
    GiftCardCurrentStateRepository currentStateRepository,
    GiftCardMessageSender giftCardMessageSender
  ) {
    this.eventPublisher = new EventPublisher<GiftCardEvent>()
      .register(new GiftCardCurrentStateUpdater(currentStateRepository))
      .register(new MessageSenderEventHandler(giftCardMessageSender))
  }

Event handler

public class GiftCardCurrentStateUpdater implements EventHandler<GiftCardEvent> {

  @DomainEventHandler
  public void handle(GiftCardEvent event) {
    GiftCardCurrentState newState =
      switch (event) {
        case GiftCardDeclared firstEvent -> GiftCardCurrentState.from(firstEvent);
        case GiftCardEvent followingEvent -> updateState(followingEvent);
      };

    currentStateRepository.save(newState);
  }

GiftCardCurrentState.from()

@QueryModel
public record GiftCardCurrentState(...) {

  public static GiftCardCurrentState from(GiftCardDeclared giftCardDeclared) {
    return new GiftCardCurrentState(
      giftCardDeclared.barcode(),
      giftCardDeclared.amount(),
      giftCardDeclared.shoppingStore(),
      false
    );
  }

GiftCardCurrentStateUpdater.updateState()

public class GiftCardCurrentStateUpdater implements EventHandler<GiftCardEvent> {

  private GiftCardCurrentState updateState(GiftCardEvent giftCardEvent) {
    GiftCardCurrentState currentState = currentStateRepository
      .get(giftCardEvent.barcode())
      .orElseThrow();
    return currentState.apply(giftCardEvent);
  }

GiftCardCurrentState.apply()

@QueryModel
public record GiftCardCurrentState(

  public GiftCardCurrentState apply(GiftCardEvent giftCardEvent) {
    return switch (giftCardEvent) {
      case PaidAmount paidAmount -> this.withRemainingAmount(
          remainingAmount.subtract(paidAmount.amount())
        );
      case GiftCardExhausted __ -> this.withExhausted(true);
      case GiftCardDeclared __ -> throw new IllegalStateException(
        "GiftCardDeclared event is not expected as an update event"
      );
    };
  }
}

Recap

Pause ✋

Query model

cqrs query

Query model

cqrs query&handler

Command model

cqrs command

Command model

cqrs command&event

Command model

cqrs command&events

Command model

cqrs command&eventstore

Query model

cqrs query&handler

Command Query Responsibility Segregation

cqrs

Command Query Responsibility Segregation

cqrs aggregate

Prendre des décisions en Event Sourcing

Focus sur la commande de paiement

@AggregateRoot
public class GiftCard {

  public List<GiftCardEvent> pay(Payment payment) {

    // Et si le paiement est supérieur au montant restant ?

    PaidAmount paidAmount = new PaidAmount(
      this.barcode(),
      this.nextSequenceId(),
      payment.amount(),
      payment.date()
    );

    return List.of(paidAmount);
  }

new GiftCard()

@AggregateRoot
public class GiftCard {

  public GiftCard(GiftCardHistory history) {
    // ...
  }

Encore une projection

@AggregateRoot
public class GiftCard {

  private final DecisionProjection decisionProjection;

  public GiftCard(GiftCardHistory history) {
    decisionProjection = DecisionProjection.from(history);
  }

DecisionProjection

@AggregateRoot
public class GiftCard {

  private record DecisionProjection(
    Barcode barcode,
    SequenceId currentSequenceId,
    Amount remainingAmount
  ) {

DecisionProjection.from()

    public static DecisionProjection from(GiftCardHistory history) {
      GiftCardDeclared firstEvent = history.start();

      return history
        .followingEvents()
        .stream()
        .reduce(
          new DecisionProjection(
            firstEvent.barcode(),
            firstEvent.sequenceId(),
            firstEvent.amount()
          ),
          DecisionProjection::accumulator,
          new SequentialCombiner<>()
        );
    }

DecisionProjection.accumulator()

    private static DecisionProjection accumulator(
      DecisionProjection currentProjection,
      GiftCardEvent event
    ) {
      return switch (event) {
        case GiftCardDeclared __ -> currentProjection;
        case PaidAmount paidAmount -> currentProjection
          .withRemainingAmount(
            currentProjection.remainingAmount().subtract(paidAmount.amount())
          )
          .withSequenceId(paidAmount.sequenceId());
        case GiftCardExhausted giftCardExhausted -> currentProjection.withSequenceId(
          giftCardExhausted.sequenceId()
        );
      };
    }

Utilisation de la projection de décision

@AggregateRoot
public class GiftCard {

  public List<GiftCardEvent> pay(Payment payment) {
    if (this.hasNotEnoughBalance(payment.amount())) {
      throw new InsufficientRemainingAmountException(this.barcode());
    }
    PaidAmount paidAmount = new PaidAmount(
      // ...
    );

    return List.of(paidAmount);
  }

  private boolean hasNotEnoughBalance(Amount amount) {
    return decisionProjection.remainingAmount.isLessThan(amount);
  }
}

🍹🌴💅

objection

"Statistiques des dépenses par jour de la semaine"

weekly statistics

Faisable en state sourcing ?

  • Informations nécessaires ?

  • Reprise de données ?

  • Table d’audit ?

Faisable en event sourcing ?

Codons-le ensemble !

🧑‍💻

Event sourcing - Avantages

  • Pas de perte d’information

  • Support super simple

  • Résilient aux nouvelles fonctionnalités

Event sourcing - Inconvénients

  • Beaucoup d’information

  • Pas comme d’habitude

  • Inutilisable en l’état

Event sourcing - Inconvénients

  • Beaucoup d’information

  • Pas comme d’habitude

  • Besoin de projections

Merci

🙇🙇‍♂️

Références

Feedback

qrcode feedback