Upgrade to Pro — share decks privately, control downloads, hide ads and more …

KATOWICE // Adam Polak - Event Sourcing Buildin...

KATOWICE // Adam Polak - Event Sourcing Building Blocks

Presentation during Kariera IT in Katowice 7.04.2018

Avatar for KarieraIT

KarieraIT

April 07, 2018

Other Decks in Programming

Transcript

  1. EVENT SOURCING - BUILDING BLOCK AGENDA ▸ EVENT SOURCING BASICS

    ▸ WHAT ABOUT STORE? ▸ HOW TO HANDLE EVENTS? ▸ VERSIONING ▸ PERFORMANCE ▸ QUESTIONS
  2. WHAT IS EVENT SOURCING? ▸ CREATE TASK ▸ FETCH TASKS

    ▸ UPDATE TASK ▸ REMOVE TASK TASKS MANAGEMENT
  3. TASK WHAT IS EVENT SOURCING? class Task { constructor(id, title,

    description) { this._id = id; this._title = title; this._description = description; } get id() { … } set id(newId) { … } }
  4. TASK UPDATING WHAT IS EVENT SOURCING? task.title(‘new-title’); tasksRepository.persist(task); | id

    | title | description | create_date | update_date | | 1 | new-title | some-description | 2018-01-01 | 2018-01-11 | const task = tasksRepository.findById(id);
  5. WHAT IS EVENT SOURCING? ▸ WHERE IS MY HISTORY? ▸

    WHAT IF DB SCHEMA CHANGES? MIGRATIONS? ▸ HOW TO RE-TRACE MISTAKES? PROBLEMS
  6. { type: ‘TASK_CREATED, aggregateId: 1, version: 1, createDate: ‘2018-01-01 12:23:34’,

    payload: { title: ‘some-title’ } } { type: ‘TASK_UPDATED, aggregateId: 1, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘new-title’ } } WHAT ABOUT STORE? EVENTS
  7. WHAT IS EVENT SOURCING? AGGREGATE ROOT class Task { constructor(id)

    { this.id = id; this.recentEvents = []; this.aggregateVersion = 0; } static fromEvents(aggregateId, events) { … } updateTitle(title) { const taskUpdatedEvent = taskUpdatedEvent(this.id, ++this.aggregateVersion, { title }); this.recentEvents.push(taskUpdatedEvent); this.apply(taskUpdatedEvent); } apply(event) {…} getRecentEvents() { … } }
  8. REBUILD FROM EVENTS { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1,

    version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’ } } { id: 1, title: ‘some-title’, description: null, aggregateVersion: 1 } { type: ‘TASK_UPDATED, aggregateId: 1, aggregateVersion: 2, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘new-title’ } } { id: 1, title: ‘new-title’, description: null, aggregateVersion: 2 } { type: ‘TASK_UPDATED, aggregateId: 1, aggregateVersion: 3, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { description: ‘new-description’ } } { id: 1, title: ‘new-title’, description: ‘new-description’, aggregateVersion: 3 } WHAT IS EVENT SOURCING?
  9. WHAT IS EVENT SOURCING? REBUILD FROM EVENTS fromEvents(aggregateId, events) {

    const task = new Task(aggregateId); for (let event of events) { task.apply(event); } return task; }
  10. WHAT IS EVENT SOURCING? APPLY EVENTS apply(event) { const handler

    = this.handlers.find(handler => handler.type === event.type); if (typeof handler !== 'undefined') { handler.handle.call(this, event); } else { throw new AppException(`Missing handler for event with type ${event.type}`); } } this.handlers = [ taskCreatedHandler, taskUpdatedHandler ];
  11. WHAT IS EVENT SOURCING? HANDLE EVENT const eventTypes = require('../events/types');

    module.exports = ({ type: eventTypes.TASK_CREATED, handle: function(event) { this.aggregateVersion = event.payload.aggregateVersion; this.title = event.payload.title; this.description = event.payload.description; this.owner = event.payload.owner } });
  12. WHAT ABOUT STORE? TASK UPDATING function patchTask(req, res, next) {

    try { const events = await req.eventsStore.getEventsFor(req.params.taskId); const task = Task.fromEvents(req.params.taskId, events); if (req.body.title) { task.updateTitle(req.body.title); } req.eventsStore.push(task.getRecentEvents()); res.status(204).send(); } catch (e) { next(e); } }
  13. RETURN GENERATED EVENTS WHAT IS EVENT SOURCING? getRecentEvents() { const

    events = [...this.recentEvents]; this.recentEvents = []; return events; }
  14. WHAT ABOUT STORE? EVENTS STORE class EventStore { constructor(connection, eventsBus,

    upcasters) { … } async push(events) { for(let event of events) { await this.collection.insert(event); this.eventsBus.publish(event); } } getEventsFor(aggregateId) { const snapshot = await this.snapshots.findLatestFor(aggregateId); return this.eventsCollection.find({ aggregateId, createDate: { “$gt”: snapshot.createDate } }) .sort({ "payload.createDate": 1 }) .then(events => [snapshot, …events].map(event => this.upcasters.upcast(event))); } }
  15. | id | aggregateId | aggregateVersion | type | createDate

    | payload | version | | 10 | 1 | 10 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 3 | | 9 | 1 | 9 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 3 | | 8 | 1 | 8 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 2 | | 7 | 1 | 7 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 2 | | 6 | 1 | 6 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 2 | | 5 | 1 | 5 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 2 | | 4 | 1 | 4 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 1 | | 3 | 1 | 3 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 1 | | 2 | 1 | 2 | TASK_UPDATED | 2018-01-02 | { title: ‘new-title’ } | 1 | | 1 | 1 | 1 | TASK_CREATED | 2018-01-02 | { title: ‘new-title’ } | 1 | EVENTS STORE WHAT ABOUT STORE?
  16. HOW TO HANDLE EVENTS? ▸ HOW DO WE FILTER TASKS?

    ▸ HOW DO I FETCH TASK? PROBLEMS
  17. EVENTS BUS class EventStore { constructor(connection, eventsBus, upcasters) { …

    } async push(events) { for(let event of events) { await this.collection.insert(event); this.eventsBus.publish(event); } } getEventsFor(aggregateId) { … } } HOW TO HANDLE EVENTS?
  18. EVENTS BUS HOW TO HANDLE EVENTS? { type: ‘TASK_CREATED, aggregateId:

    1, aggregateVersion: 1, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’ } } EVENTS BUS PROJECTORS MAILER HANDLER
  19. EVENTS BUS HOW TO HANDLE EVENTS? class EventBus { constructor()

    { this.subscribers = []; } subscribe(event, handler) { if (typeof this.subscribers[`${event}`] === 'undefined') { this.subscribers[`${event}`] = []; } this.subscribers[`${event}`].push(handler); } publish(event) { if (this.subscribers[`${event.type}`]) { this.subscribers[`${event.type}`].forEach((handler) => handler(event)); } } }
  20. HOW TO HANDLE EVENTS? EVENTS PROJECTION const handler = connection

    => event => { if (event.type === eventTypes.TASK_CREATED) { return connection.collection(COLLECTION).insert({ … }); } if (event.type === eventTypes.TASK_UPDATED) { return connection.collection(COLLECTION).update({ … }); } };
  21. HOW TO HANDLE EVENTS? EVENTS PROJECTION | id | title

    | description | create_date | update_date | | 1 | new-title | some-description | 2018-01-01 | 2018-01-11 |
  22. HOW TO HANDLE EVENTS? READMODEL REBUILD mongoClient .connect(`mongodb://${process.env.DB_HOST}:${process.env.DB_PORT}/tsh`) .then(async (connection)

    => { const eventStore = new EventStore(connection, new EventBus(), new Upcasters()); const projector = activeTasksProjector.rebuilder(connection); const events = await eventStore.getAll(); projector(events).then(() => process.exit(0)); });
  23. EVENTS VERSIONING UPCASTING { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1,

    version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’ } } { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1, version: 2, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’, status: ‘pending’ } }
  24. EVENTS VERSIONING UPCASTING { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1,

    version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’ } } { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1, version: 2, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’, status: ‘pending’ } } UPCASTER
  25. EVENTS VERSIONING UPCASTER class EventStore { constructor(connection, eventsBus, upcasters) {

    … } async push(events) { for(let event of events) { await this.collection.insert(event); this.eventsBus.publish(event); } } getEventsFor(aggregateId) { const snapshot = await this.snapshots.findLatestFor(aggregateId); return this.eventsCollection.find({ aggregateId, createDate: { “$gt”: snapshot.createDate } }) .sort({ "payload.createDate": 1 }) .then(events => events.map(event => this.upcasters.upcast(event))); } }
  26. EVENTS VERSIONING UPCASTER const eventTypes = require('../task/events/types'); module.exports = {

    canUpcast: (event) => event.version === 1 && event.type === eventTypes.TASK_CREATED, upcast: (event) => ({ ...event, version: 2, payload: { ...event.payload, status: 'pending' } }) };
  27. PERFORMANCE SNAPSHOTS { type: ‘TASK_CREATED, aggregateId: 1, aggregateVersion: 1, version:

    1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’ } } { type: ‘TASK_UPDATED, aggregateId: 1, aggregateVersion: 2, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { description: ‘new-description’ } } { type: ‘TASK_SNAPSHOT, aggregateId: 1, aggregateVersion: 2, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { title: ‘some-title’, description: ‘new-description’ } }
  28. class EventStore { async push(events) { for(let event of events)

    { (…) if (event.aggregateVersion % SNAPSHOT_COUNTER === 0) { await this.snapshots.createForAggregate(event.aggregateId); } } } getEventsFor(aggregateId) { const snapshot = await this.snapshots.findLatestFor(aggregateId); return this.eventsCollection.find({ aggregateId, createDate: { “$gt”: snapshot.createDate } }) .sort({ "payload.createDate": 1 }) .then(events => [snapshot, …events].map(event => this.upcasters.upcast(event))); } } SNAPSHOTS PERFORMANCE