Lyon Craft 2025
Du concept au code
Lyon Craft 2025
Aurélien Mino
Cédric Chateau
Senior Dev @
❤️ CQRS / ES
💔 Acronymes
Senior Dev @
❤️ CQRS / Event Sourcing
💔 Acronymes (non expliqués)
Lead Tech @
❤️ Open-source
Patterns méconnus / peu utilisés
Patterns méconnus / peu utilisés
Cédric adore CQRS / ES 😄
Patterns méconnus / peu utilisés
Cédric adore CQRS / ES 😄
Partager à travers un exemple concret
Facile à comprendre
Facile à lire
On fait tout le temps comme ça
Perte d’information
Pas de traçabilité
Obligation d’anticiper les besoins futurs
Début
Début … événement … événement
State sourcing
Event sourcing
🔍
→ Les événements
L’agrégat
L’application service
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 {}
🔍
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);
}
}
🔍
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);
}
}
Paginée 🤔
Triée 😮
Filtrée 😱
🔍
@QueryModel
public record GiftCardCurrentState(
Barcode barcode,
Amount remainingAmount,
ShoppingStore shoppingStore,
boolean exhausted
) {
@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))
}
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"
);
};
}
}
Pause ✋
@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) {
// ...
}
@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()
);
};
}
@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);
}
}
Informations nécessaires ?
Reprise de données ?
Table d’audit ?
🧑💻
Pas de perte d’information
Support super simple
Résilient aux nouvelles fonctionnalités
Beaucoup d’information
Pas comme d’habitude
Inutilisable en l’état
Beaucoup d’information
Pas comme d’habitude
Besoin de projections
🙇🙇♂️
CQRS
Event Modeling : https://eventmodeling.org/
Event Sourcing
Le code complet de notre appli : https://github.com/murdos/gift-cards-cqrs-es