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

CQRS, Fonctionel, Event Sourcing & Domain Driven Design — MUG Lyon 2017

CQRS, Fonctionel, Event Sourcing & Domain Driven Design — MUG Lyon 2017

Avec autant de buzzwords dans le titre, laissez-moi expliciter 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 pourquoi ont-ils 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ôture, nous verrons les erreurs les plus courantes rencontrées lors de l’implémentation d’un modèle en event sourcing. Et nous terminerons avec une présentation de l’architecture que nous utilisons en interne chez Arpinum pour nos clients.

Take away:

• Utiliser CQRS (sans event-sourcing) pour simplifier la gestion de la persistance dans son application.

• Comprendre le modèle d’états derrière Redux (utilisé massivement par la communauté ReactJs).

• 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

February 23, 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);
 repository.add(hub);
 }
 
 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. F U N C T I O N A L

    P R O G R A M M I N G S TAT E D E F I N I T I O N
  29. What is a state ?

  30. A VA L U E O V E R T

    I M E String var; 
 var = "something";
 var = "something else"; something something else time
  31. A P P L I C AT I O N

    S H A R E D S TAT E M A N A G E R T H E D ATA B A S E
  32. T H I S L E A D T O

    P L O P P L A C E O R I E N T E D P R O G R A M M I N G « Where new informations replace old » - R I C H H I C K E Y
  33. G E T Y O U T I M E

    S E N S I T I V E A P P L I C AT I O N A B C D A B C D Y O U N E E D T H AT E V E RY T H I N G H A P P E N S I N T H E R I G H T O R D E R
  34. This is why a debugger is a time machine

  35. A N D B U S I N E S

    S M E M O RY L O S S - « What was the last dropped shopping cart ? » - « We don’t know, the data have already been replaced »
  36. T H E S O L U T I O

    N ? W E R E M O V E T H E T I M E F R O M T H E E Q U AT I O N « VA L U E O R I E N T E D P R O G R A M M I N G » time x value = variable value = constant
  37. F U N C T I O N A L

    PA R A D I G M « Functional programming makes concurrency composable — something that’s virtually impossible with other programming paradigms » - B A R T O S Z M I L E W S K I
  38. F U N C T I O N A L

    P R O G R A M M I N G S TAT E M A N A G E M E N T
  39. Y O U C A N C R E AT

    E N E W VA L U E VA R I A B L E C O N S TA N T
  40. Y O U C A N C R E AT

    E N E W VA L U E VA R I A B L E C O N S TA N T time
  41. Y O U C A N C R E AT

    E N E W VA L U E VA R I A B L E C O N S TA N T but older values still exists time
  42. S H O U L D I P L O

    P ? public class Circle {
 
 public Circle(int radius, int posX, int posY) {
 this.radius = radius;
 this.posX = posX;
 this.posY = posY;
 }
 
 public void move(int posX, int posY) {
 this.posX = posX;
 this.posY = posY;
 }
 
 private int radius;
 private int posX;
 private int posY;
 }
  43. O R S H O U L D I N

    O T ? public class Circle {
 
 public Circle(int radius, int posX, int posY) {
 this.radius = radius;
 this.posX = posX;
 this.posY = posY;
 }
 
 public Circle move(int posX, int posY) {
 return new Circle(radius, posX, posY);
 }
 
 private int radius;
 private int posX;
 private int posY;
 }
  44. I F I N E E D S TAT E

    O R C H E S T R AT I O N ? const state = [{
 id: 3,
 story: {
 author: "Arnaud LEMAIRE",
 text: "Reducer FTW"
 },
 readyForPublishing: false
 }];
 
 const publishStory = (storyId) => ({
 storyId
 });
  45. I F I N E E D S TAT E

    O R C H E S T R AT I O N ? const publishStoryReducer = (state, action) => {
 const [storyToPublish, restOfStories] = state.partitionBy( i => i.id === action.storyId );
 
 return [
 ...restOfStories,
 {
 ...storyToPublish[0],
 readyForPublishing: true
 }
 ];
 };
  46. F U N C T I O N A L

    C O R E / I M P E R AT I V E S H E L L B U S I N E S S D O M A I N ( F U N C T I O N A L ) IMPERATIVE SHELL
  47. TA K E A WAY • Avoid a whole category

    of bugs by removing state • Enable concurrency composabilitty • Push state management to the boundaries
  48. 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
  49. 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 »
  50. 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;
 }
  51. A N D A L S O S E N

    T I N A B U S eventBus.dispatch( new HubMoved( "hubId", "oldLocationId", "newLocationId" ));
  52. 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));
 }
  53. 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();
 }
  54. 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;
 
 }
  55. 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,
 this.locationId.toString(),
 newLocationId
 );
 
 this.locationId = UUID.fromString(newLocationId);
 
 return event;
 }
 UUID locationId;
 private UUID id;
 }
  56. D O M A I N E V E N

    T P R O J E C T I O N
  57. 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…
  58. 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
  59. 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();
  60. 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
  61. 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;
 }
 }
  62. 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
  63. 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 !
  64. 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…
  65. TA K E A WAY • Enable reactive business system

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

    I N G AT L E A S T …
  67. 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 ?
  68. 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;
 }
  69. 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
 );
 }
  70. 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)
 );
 }
  71. 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
  72. W H AT I S A N E V E

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

    N T S T O R E ?
  74. 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
  75. 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 = ?"
 ).execute(aggregateType, aggregateId);
 
 return records.map(records -> Serializer.deserialize( record.getValue("payload"), Event.class
 ));
 }
 }
  76. 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)
 );
 }
  77. TA K E A WAY • No more business memory

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

    A R C H Y
  79. 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]’
  80. Q & A • How do we snapshot ? •

    How do we manage version ? • How to use multiple aggregate in a handler ?
  81. 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
  82. @ L I L O B A S E |

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