Hub Maintenance Operation Session THE DOMAIN IS EXPRESS THROUGH PLAIN OLD PHP OBJECTS CATEGORIZED BETWEEN ENTITIES & VALUE OBJECTS EQUALITY ON IDENTITY, HAS LIFECYCLE EQUALITY ON VALUES, HAS NO LIFECYCLE
ONLY BE PERFORMED THROUGH THE ROOT ENFORCE INTEGRITY REFERENCE TO COLLABORATOR WITH ≠ LIFECYCLE AGGREGATE OBJECT’S SHARE A COMMON LIFECYCLE: THEY CAN BE SAVED & FETCHED AS A WHOLE
O T A R E B U S I N E S S O B J E C T public class Hub implements AggregateRootWithUUID { static public Hub create( String productReference, UUID locationId ) { Hub instance = new Hub(UUID.randomUUID()); instance.locationId = locationId; instance.productReference = productReference; return instance; } private Hub(UUID id){ this.id = id; //We control the ID generation } @Override public UUID id() { return id; } }
O L L A B O R AT O R S H U B M A I N T E N A N C E O P E R AT I O N M A I N T E N A N C E O P E R AT I O N M A I N T E N A N C E O P E R AT I O N public class MaintenanceOperation { MaintenanceOperation( String description, Type operationType, LocalDateTime dateTime ) { this.description = description; this.operationType = operationType; this.dateTime = dateTime; } }
C O N S I S T E N C Y public class Hub implements AggregateRootWithUUID { //… public void addMaintenanceOperation( String description, MaintenanceOperation.Type type ) { maintenanceOperations = maintenanceOperations.append( new MaintenanceOperation( description, type, LocalDateTime.now())); } }
E F E R E N C E T O O T H E R A G G R E G AT E public class Hub implements AggregateRootWithUUID { static public Hub create( String productReference, UUID locationId ) { final Hub instance = new Hub(UUID.randomUUID()); instance.locationId = locationId; instance.productReference = productReference; return instance; } private UUID locationId; }
D T O public class NewMaintenanceOperationUseCase implements UseCase<Void> { public NewMaintenanceOperationUseCase( String switchId, String description, String type ) { this.switchId = switchId; this.description = description; this.type = type; } public String switchId; public String description; public String type; }
AT E D H A N D L E R public class NewMaintenanceOperationUseCaseHandler implements UseCaseHandler<Void, NewMaintenanceOperationUseCase> { public NewMaintenanceOperationUseCaseHandler( SwitchRepository repository ) { this.repository = repository; } @Override public void handle(NewMaintenanceOperationUseCase command) { Hub hub = repository.get(UUID.fromString(command.switchId)); Type maintenanceOperationType = Type.valueOf(command.type); hub.addMaintenanceOperation( command.description, maintenanceOperationType ); repository.add(aSwitch); } private final SwitchRepository repository; }
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 »
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)
@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
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; }
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; }
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
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
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; } }
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; } }
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
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
E A B S T R A C T I O N ( P S E U D O M A P I N T E R FA C E ) 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
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
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"); }
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; }
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
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); } }
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
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; }
I P L E H A N D L E R public EventBus(List<? extends EventHandler> handlers) { this.handlers = handlers.groupBy( EventHandler::listenTo() )); } public <C extends Event> Void dispatch(C event) { handlers.get(event.getClass()) .forEach(h -> h.handle(event)); }
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; }
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; } }
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; } }
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; }
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; }); } }
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…
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();
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
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; } }
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
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 !
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…
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(); }
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; }
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 ?
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; }
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 ); }
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
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 )); } }
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]’