Save 37% off PRO during our Black Friday Sale! »

CQRS, Fonctionnel, Event Sourcing & Domain Driven Design — Breizhcamp 2017

CQRS, Fonctionnel, Event Sourcing & Domain Driven Design — Breizhcamp 2017

Avec autant de buzzwords dans le titre, explicitons le menu :


– Nous commencerons avec une étude des principes du CQRS et la notion de projection pour construire les modèles de données dédiées à la lecture, le tout avec un datastore traditionnel (relationnel). 
– Nous continuerons avec le concept d’état en programmation fonctionnelle, et comment les gérer au sein d’une application tout en respectant le principe d’immutabilité. Et comment ils ont transformé la gestion d’états pour la construction d’interface utilisateur. 
– Dans un troisième temps, nous nous intéresserons aux évènements du domaine-métier dans le Domain Driven Design et comment ceux-ci s’intègrent dans la mécanique de construction des projections. 
– Enfin, nous assemblerons toutes ces notions pour faire apparaitre l’« event sourcing » comme modèle de persistance pour nos données. 
– Pour clôturer, nous verrons les erreurs les plus courantes rencontrées lors de l’implémentation d’un modèle en event sourcing.

Take away: 
– Utiliser CQRS (sans event-sourcing) pour simplifier la gestion de la persistance dans son application. 
– Comprendre comment gérer des états dans un contexte fonctionnel 
– Gérer facilement les évènements-métier au sein d’une architecture DDD. 
– Savoir comment implémenter correctement un système basé sur l’event sourcing.


Beb422437c1dfb5366f197919e41ac50?s=128

Arnaud LEMAIRE
PRO

April 20, 2017
Tweet

Transcript

  1. C Q R S , E V E N T

    S O U R C I N G & F U N C T I O N A L P R O G R A M M I N G T H E B U Z Z W O R D C O N F E R E N C E
  2. @ L I L O B A S E –

    W W W. A R P I N U M . F R Arnaud LEMAIRE | Arpinum
  3. C Q R S C O M M A N

    D Q U E RY R E S P O N S I B I L I T Y S E G R E G AT I O N
  4. C H A N G E S O M E

    T H I N G E V E RY T H I N G S TA R T S W I T H U S E C A S E S U S E R I want to … G E T I N F O A B O U T S O M E T H I N G (read) (mutation) C O M M A N D Q U E RY « I need information to make a decision » « I made a decision »
  5. C O M M A N D S & Q

    U E R I E S A R E D T O class RelocateHubCommand implements Command<Void> {
 
 public RelocateHubCommand(
 String hubId, String newLocationId ) {
 this.hubId = hubId;
 this.newLocationId = newLocationId;
 }
 
 @NotEmpty
 public String hubId;
 
 @NotEmpty
 public String newLocationId;
 
 } A command returns no business data to the client (can return a status & an id)
  6. S E N T I N A B U S

    @RestController
 class HubResource {
 
 @Autowired
 public HubResource(CommandBus commandBus) {
 this.commandBus = commandBus;
 }
 
 @PutMapping("/api/hub/location")
 public Void relocate(Request request) {
 Map<String, String> requestBody = Serializer.deserialize(request.body()); 
 return this.commandBus.dispatch( new RelocateHubCommand( requestBody.get("hubId"), requestBody.get("newLocationId") ));
 }
 }
  7. S E N T I N A B U S

    @RestController
 class HubResource {
 
 @Autowired
 public HubResource(CommandBus commandBus) {
 this.commandBus = commandBus;
 }
 
 @PutMapping("/api/hub/location")
 public Void relocate(Request request) {
 Map<String, String> requestBody = Serializer.deserialize(request.body()); 
 return this.commandBus.dispatch( new RelocateHubCommand( requestBody.get("hubId"), requestBody.get("newLocationId") ));
 }
 } Almost no stickiness to a framework
  8. D I S PAT C H E D T O

    A S P E C I F I C H A N D L E R @Component
 public class CommandBus {
 
 @Autowired
 public CommandBus(List<? extends Handler> handlers) {
 this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) );
 }
 
 public <R, C extends Command<R>> R dispatch(C command) {
 return handlers .get(command.getClass()) .handle(command);
 }
 
 private final Map<Class, Handler> handlers;
 }
  9. D I S PAT C H E D T O

    A S P E C I F I C H A N D L E R @Component
 public class CommandBus {
 
 @Autowired
 public CommandBus(List<? extends Handler> handlers) {
 this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) );
 }
 
 public <R, C extends Command<R>> R dispatch(C command<R>) {
 return handlers .get(command.getClass()) .handle(command);
 }
 
 private final Map<Class, Handler> handlers;
 }
  10. T H E F U L L P I P

    E L I N E U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R one per query one per command Q U E RY B U S C O M M A N D B U S
  11. M I D D L E WA R E @Component

    public class BusValidationMiddleware<T> implements BusMiddleware<T, Message<T>> { @Autowired
 public BusValidationMiddleware( BusMiddleware next, Validator validator ) {
 this.next = next;
 this.validator = validator;
 }
 
 public T handle(Message<T> message) {
 Violation violations = validator.validate(message);
 if(violations.size() > 1)
 throw new ArgumentException(violations.toArray());
 
 return next.handle(message);
 }
 }
  12. C O M P O S E D I N

    T H E B U S U S E R C O M M A N D Q U E RY new BusValidationMiddleware(
 new BusTransactionMiddleware(
 new BusDispatcher( Q U E RY H A N D L E R C O M M A N D H A N D L E R Q U E RY B U S C O M M A N D B U S VA L I D AT I O N C A C H E VA L I D AT I O N U N I T O F W O R K
  13. C Q R S T H E C O M

    M A N D
  14. C O M M A N D H A N

    D L I N G @Component
 public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> {
 @Autowired
 public RelocateHubCommandHandler(HubRepository repo) {
 this.repository = repo;
 }
 
 @Override
 public Void handle(RelocateHubCommand command) {
 Hub hub = repository.getSiteInformation();
 hub.relocateTo(command.newLocationId);
 }
 
 public Class listenTo() {
 return RelocateHubCommand.class;
 }
 }
  15. P E R S I S T E N C

    E A B S T R A C T I O N public interface Repository<TId, TRoot> {
 TRoot get(TId id);
 
 void add(TRoot root);
 
 void delete(TRoot root);
 
 List<TRoot> getAll();
 } The persistence mechanism save snapshot of the application state
  16. P E R S I S T E N C

    E A B S T R A C T I O N public class HubRepository implements Repository<UUID, Hub> {
 
 public HubRepository(Store store)
 this.store = store;
 
 public Hub get(UUID uuid)
 return store.findById(uuid);
 
 public void add(Hub hub)
 store.upsert(hub);
 
 public void delete(Hub hub)
 store.remove(hub);
 
 public Seq<Hub> getAll()
 return store.findAll();
 } We fetch and save aggregate as a whole
  17. C O M M A N D H A N

    D L I N G @Component
 public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> {
 @Autowired
 public RelocateHubCommandHandler(HubRepository repo) {
 this.repository = repo;
 }
 
 @Override
 public Void handle(RelocateHubCommand command) {
 Hub hub = repository.get(command.hubId);
 hub.relocateTo(command.newLocationId);
 }
 
 public Class listenTo() {
 return RelocateHubCommand.class;
 }
 }
  18. H A N D L E R S W O

    R K O N A G G R E G AT E public class Hub {
 public Hub(UUID id) { this.id = id; }
 
 public Hub() { this.id = UUID.randomUUID(); }
 
 public UUID id() { return id; } 
 //...
 
 public void relocateTo(String newLocationId) {
 locationId = UUID.fromString(newLocationId);
 }
 
 //...
 
 private UUID locationId;
 private UUID id;
 } We keep under control the aggregate id creation
  19. H A N D L E R I N T

    E R N A L S S T O R E D B A G G R E G AT E S T O R E D B fetched flushed by the unit of work middleware domain operations handler(state, command) = state’ saved
  20. T E S T I N G @Test
 public void

    it_relocates_a_hub() {
 store = new InMemoryStore();
 command = new RelocateHubCommand("hubId", "newLocationId");
 handler = new RelocateHubCommandHander( new HubRepository(store) );
 
 handler.handle(command); 
 expectedHub = store.get("hubId");
 assertThat(expectedHub.location()).equals("newLocationId");
 }
  21. C Q R S T H E Q U E

    RY
  22. C O M M A N D S & Q

    U E R I E S A R E D T O class FindHubByLocationPathQuery implements Query<Void> {
 
 public FindHubByLocationPath(
 String locationPath, ) {
 this.locationPath = locationPath;
 }
 
 @NotEmpty
 public String locationPath;
 
 }
  23. Q U E RY H A N D L I

    N G @Component
 public class FindHubByLocationPathQueryHander extends SqlHandler<Hub, FindHubByLocationPathQuery> {
 
 @Autowired
 public FindHubByLocationPathQueryHander(SqlConnection sql) {
 this.sql = sql;
 }
 
 @Override
 public Installation handle(FetchAllLocationsQuery query) {
 Record hub = sql.prepare( "SELECT * FROM hub JOIN location ON hub.locationId = location.id WHERE location.path = %s" ).execute(query.locationPath);
 return Serializer.deserialize(hub);
 }
 } We receive a direct connection to the datastore
  24. Q U E RY H A N D L I

    N G @Component
 public class FindHubByLocationPathQueryHander extends SqlHandler<Hub, FindHubByLocationPathQuery> {
 
 @Autowired
 public FindHubByLocationPathQueryHander(SqlConnection sql) {
 this.sql = sql;
 }
 
 @Override
 public Installation handle(FetchAllLocationsQuery query) {
 Record hub = sql.prepare( "SELECT * FROM hub JOIN location ON hub.locationId = location.id WHERE location.path = %s" ).execute(query.locationPath);
 return Serializer.deserialize(hub);
 }
 }
  25. C Q R S W R A P P I

    N G U P
  26. T H E S A M E D ATA S

    T O R E C A N B E S H A R E D U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P E R S I S T E N C E while maintaining a complete segregation through the whole application Q U E RY B U S C O M M A N D B U S
  27. TA K E A WAY • Massive simplification of persistence

    mechanisms • Unit testing in a breeze • Enable business use case discovery • Drive task based UI instead of CRUD thinking
  28. D O M A I N E V E N

    T S T I L L N O T E V E N T S O U R C I N G
  29. E V E N T ≠ C O M M

    A N D C O M M A N D E V E N T Intent : « do something » Event : « something happened »
  30. E V E N T A R E A L

    S O D T O class HubMovedEvent implements Event {
 
 public HubMovedEvent(
 String hubId, String oldLocationId, String newLocationId ) {
 this.hubId = hubId; this.oldLocationId = oldLocationId;
 this.newLocationId = newLocationId;
 }
 
 public String hubId;
 
 public String newLocationId; public String oldLocationId;
 }
  31. A N D A L S O S E N

    T I N A B U S eventBus.dispatch( new HubMoved( "hubId", "oldLocationId", "newLocationId" ));
  32. B U T W I T H M U LT

    I P L E H A N D L E R public EventBus(List<? extends EventHandler> handlers) {
 this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) );
 }
 
 public <C extends Event> Void dispatch(C event) {
 handlers.filter(t -> t._1 == event.getClass())
 .forEach(h -> h.handle(event));
 }
  33. A N E V E N T I S T

    H E R E S U LT O F A B U S I N E S S O P E R AT I O N public class Hub {
 
 public Event relocateTo(String newLocationId) {
 HubMovedEvent event = new HubMovedEvent(
 this.locationId.toString(),
 newLocationId
 );
 
 this.locationId = UUID.fromString(newLocationId);
 
 return event;
 }
 UUID locationId;
 private UUID id;
 }
  34. R E T U R N E D B Y

    C O M M A N D H A N D L E R S @Component
 public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> {
 @Autowired
 public RelocateHubCommandHandler(HubRepository repo) {
 this.repository = repo;
 }
 
 @Override
 public Void handle(RelocateHubCommand command) {
 Hub hub = repository.get(command.hubId);
 hub.relocateTo(command.newLocationId);
 }
 
 public Class listenTo() {
 return RelocateHubCommand.class;
 }
 }
  35. R E T U R N E D B Y

    C O M M A N D H A N D L E R S @Component
 public class RelocateHubCommandHandler implements CommandHandler<Void, RelocateHubCommand> {
 @Autowired
 public RelocateHubCommandHandler(HubRepository repo) {
 this.repository = repo;
 }
 
 @Override
 public Tuple2<Void, List<Event> handle(RHC command) {
 Hub hub = repository.get(command.hubId);
 return Tuple.of(
 void,
 List.of(hub.relocateTo(command.newLocationId)
 );
 }
 
 public Class listenTo() {
 return RelocateHubCommand.class;
 }
 }
  36. D I S PAT C H E D B Y

    T H E C O M M A N D B U S @Component
 public class CommandBus {
 
 @Autowired
 public CommandBus(List<? extends Handler> handlers) {
 this.handlers = handlers.toMap( h -> Tuple.of(h.listenTo(), h) );
 }
 
 public <R, C extends Command<R>> R dispatch(C command<R>) {
 return handlers .get(command.getClass()) .handle(command);
 }
 
 private final Map<Class, Handler> handlers;
 }
  37. D I S PAT C H E D B Y

    T H E C O M M A N D B U S @Component public class CommandBus { @Autowired public CommandBus(
 Collection<? extends CommandHandler> handlers, EventBus eventBus) { //… this.eventBus = eventBus; } public <R, C extends Command<R>> Try<R> dispatch(C command) { return handlers.get(command.getClass()) .map(h -> execute(h, command)) .getOrElse(() -> Try.failure( new HandlerNotFoundException(command))); } private <R, C extends Command<R>> Try<R> execute(
 CommandHandler<R, C> h, C command) { return Try.of(() -> { Tuple2<R, List<Event>> result = h.handle(command); result._2.forEach(eventBus::publish); return result._1; }); } }
  38. D O M A I N E V E N

    T P R O J E C T I O N
  39. L E T ’ S G O B A C

    K I N T H E C O M M A N D H A N D L E R U S E R C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R Q U E RY B U S C O M M A N D B U S we are here…
  40. O U R C O M M A N D

    H A N D L E R A G G R E G AT E E V E N T S A G G R E G AT E domain operations handler(state, command) = (state’, [event]) domain event
  41. A N D R E T U R N S

    T H E M public List<Event> handle(RelocateHubCommand command) {
 Hub hub = repository.getHub(command.hubId);
 events.add(hub.relocateTo(command.newLocationId));
 
 return events;
 }
 
 private List<Event> events = List.empty();
  42. T O B E D I S PAT C H

    E D B Y A M I D D L E WA R E C O M M A N D C O M M A N D H A N D L E R C O M M A N D B U S VA L I D AT I O N U N I T O F W O R K E V E N T D I S PAT C H E R S E N D E M A I L L A U N C H C O M M A N D
  43. W E C A N L I S T E

    N E V E N T T O U P D AT E O T H E R D ATA S T O R E public class HubLocationsUpdater implements EventHandler<HubMovedEvent> {
 
 public HubLocationsUpdater(Connection sql) {
 this.sql = sql;
 }
 
 public void handle(HubMovedEvent event) {
 sql.createStatement().executeUpdate(
 "UPDATE hub_location SET location_id = ? WHERE hub_id = ?"
 ).execute(event.newLocationId, event.hubId);
 }
 
 public Class<? extends Event> listenTo() {
 return HubMovedEvent.class;
 }
 }
  44. W E C A N C R E AT E

    A R E A D M O D E L I N T H E S A M E D ATA S T O R E C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P R O J E C T I O N U P D AT E R E V E N T D I S PAT C H E R P E R S I S T E N C E W R I T E M O D E L R E A D M O D E L
  45. O R I N D I F F E R

    E N T O N E C O M M A N D Q U E RY Q U E RY H A N D L E R C O M M A N D H A N D L E R P R O J E C T I O N U P D AT E R E V E N T D I S PAT C H E R W R I T E M O D E L R E A D M O D E L This is not event sourcing !
  46. O P E N I N G A W O

    R L D O F P O S S I B L E G E O Q U E RY S E A R C H U P D AT E R D O M A I N E V E N T G I S D B G I S U P D AT E R S Q L U P D AT E R E L A S T I C S E A R C H P O S T G R E S Q L S E A R C H Q U E RY R E L AT I O N A L Q U E RY You can adapt your datastore to your queries need plus you can even add more later…
  47. TA K E A WAY • Enable reactive business system

    • Adapt the datastore to your queries need • Suited even for legacy system
  48. E V E N T S O U R C

    I N G AT L E A S T …
  49. W E A D D S O M E M

    E TA D ATA T O E V E N T S public abstract class Event<ID, ENTITY extends AggregateRoot<ID>> {
 
 public Event(ENTITY entity) {
 aggregateId = entity.id();
 aggregateType = entity.class;
 }
 
 public ID aggregateId;
 public Class<ENTITY> aggregateType; public LocalDateTime eventDateTime = LocalDateTime.now();
 }
  50. W E A D D S O M E M

    E TA D ATA T O E V E N T S public class HubMovedEvent extends Event<UUID, Hub<UUID>> {
 
 public HubMovedEvent(
 Hub hub,
 String oldLocationId,
 String newLocationId
 ) {
 super(hub);
 this.hubId = hub.id().toString();
 this.oldLocationId = oldLocationId;
 this.newLocationId = newLocationId; 
 } 
 public String hubId;
 public String newLocationId;
 public String oldLocationId;
 
 }
  51. D O M A I N E V E N

    T A S S O U R C E O F S TAT E public class HubEventApplier implements EventApplier<Hub> {
 
 public static Hub apply(Hub root, HubMovedEvent event) {
 Hub hub = new Hub(root);
 hub.locationId = UUID.fromString(event.newLocationId);
 
 return hub;
 }
 
 } look like a reducer, no ?
  52. F R O M B U S I N E

    S S O P E R AT I O N A S M U TAT I O N public Event relocateTo(String newLocationId) {
 HubMovedEvent event = new HubMovedEvent(
 this,
 this.locationId.toString(),
 newLocationId
 );
 
 this.locationId = UUID.fromString(newLocationId);
 
 return event;
 }
  53. T O E V E N T S O U

    R C E D B U S I N E S S O P E R AT I O N S public Tuple2<Hub, Event> relocateTo(String newLocationId) {
 HubMovedEvent event = new HubMovedEvent(
 this,
 this.locationId.toString(),
 newLocationId
 );
 
 return Tuple.of(
 HubEventApplier.apply(this, event),
 event
 );
 }
  54. R E A L E V E N T A

    P P L I E R public class HubEventApplier implements EventApplier<Hub> {
 
 static Hub applyHubMovedEvent(Hub h, HubMovedEvent event) {
 Hub hub = new Hub(h);
 hub.locationId = UUID.fromString(event.newLocationId);
 return hub;
 }
 
 public static Hub apply(Hub hub, Event event) {
 return appliers .get(event.getClass()) .apply(hub, event);
 }
 
 static Map<Class, Function> appliers = HashMap.of(
 HubMovedEvent.class, (hub, event) -> applyHubMovedEvent(hub, event)
 );
 }
  55. A N D S AV E D I N E

    V E N T S T O R E C O M M A N D C O M M A N D H A N D L E R E V E N T S T O R E this is event sourcing E V E N T P E R S I S T E N C E C O M M A N D C O M M A N D H A N D L E R D B U N I T O F W O R K flush serialize
  56. W H AT I S A N E V E

    N T S T O R E ?
  57. W H AT I S A N E V E

    N T S T O R E ?
  58. E V E N T S O U R C

    I N G R E B U I L D I N G S TAT E
  59. W E G E T E V E N T

    S F O R A G I V E N A G G R E G AT E public class eventStore {
 public eventStore(Connection sql) {
 this.sql = sql;
 }
 
 public List<Event> get(String type, String id) {
 Seq<Record> records = sql.createStatement(
 "
 SELECT * FROM events 
 WHERE aggr_type = ? AND aggr_id = ? 
 ORDER BY TIMESTAMP "
 ).execute(aggregateType, aggregateId);
 
 return records.map(records -> Serializer.deserialize( record.getValue("payload"), Event.class
 ));
 }
 }
  60. A N D W E A P P LY T

    H E M public HubRepository(EventStore store) {
 
 this.store = store;
 }
 
 public Hub get(UUID uuid) {
 List<Event> events = store.get("HUB", uuid.toString());
 return events.foldLeft(
 new Hub(),
 (hub, event) -> HubEventApplier.apply(hub, event)
 );
 }
  61. TA K E A WAY • No more business memory

    loss • Pure data persistence (no more plop) • You can build new projection from past events
  62. F O L D E R H I E R

    A R C H Y
  63. H O W W E H A N D L

    E C O M M A N D • classic : 
 handler(state, command) = state’ • with domain event : 
 handler(state, command = (state’, [event]) • with event sourcing :
 handler([event], command) = [event]’
  64. Q & A • How do we snapshot ? •

    How do we manage version ? • How to use multiple aggregate in a handler ?
  65. T O G O F U R T H E

    R • CQRS Documents by Greg Young • The Value of Value by Rich Hickey • What if the user was a function by Andre Staltz
  66. @ L I L O B A S E |

    A R P I N U M . F R Thanks !