EVENT SOURCE
EVERYTHING!
EVENT SOURCE EVERYTHING!
AGENDA
▸ EVENT SOURCING BASICS
▸ WHAT ABOUT STORE?
▸ HOW TO HANDLE EVENTS?
▸ VERSIONING
▸ PERFORMANCE
▸ QUESTIONS
ADAM POLAK
HEAD OF NODE.JS
ADAM.POLAK@THESOFTWAREHOUSE.PL
POLAK.ADAM1
WHAT IS
EVENT SOURCING?
▸ CREATE EMPLOYEE
▸ ISSUE VACATION REQUEST
▸ APPROVE/DECLINE VACATION REQUEST
HR MANAGEMENT
EVENT SOURCE EVERYTHING!
NON EVENT-BASED
APPROACH
EMPLOYEE
class Employee {
constructor(id, firstName, lastName, vacationRequests) {
this._id = id;
this._firstName = firstName;
this._lastName = lastName;
this._vacationRequests = vacationRequests;
}
issueVacationRequest(from, to, comment) { // do business logic and domain validation here then return vacation req or event}
}
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST
const vacationRequest = employee.issueValidationRequest(‘2018-10-01’, ‘2018-11-01’, ‘Japan, here I come!’);
vacationRequestsRepository.persist(vacationRequest); // map vacation request from domain model to db model
const employee = employeeRepository.findById(id); // map from db model to domain model
EVENT SOURCE EVERYTHING!
| id | employee_id | from | to | comment | create_date | update_date |
| 1 | some-uuid | 2018-10-01 | 2018-11-01 | Japan, here I come! | 2018-01-11 | 2018-01-11 |
IT’S ALL ABOUT
CURRENT STATE
EVENT-BASED
APPROACH
▸ EMPLOYEE EMPLOYED
▸ VACATION REQUEST ISSUED
▸ VACATION REQUEST APPROVED
▸ VACATION REQUEST DECLINED
▸ EMPLOYEE ASSIGNED TO PROJECT
▸ EMPLOYEE DETAILS UPDATED
EVENTS
EVENT SOURCE EVERYTHING!
EVERYTHING IS
AN EVENT
HOW TO DISPLAY
EVENTS?
IT’S CQRS
ON STEROIDS
REBUILD
FROM EVENTSCOMMAND OPERATIONS
WRITE SIDE
EMIT
EVENTS
EVENT STORE
EVENT SOURCE EVERYTHING!
REQUEST
HANDLING
REBUILD
FROM EVENTSCOMMAND OPERATIONS
EMIT
EVENTS
EVENT STORE
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
const events = await eventStore.getEventsFor(employeeId);
const employee = Employee.fromEvents(employeeId, events);
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
return res.status(204).send();
} catch (error) { next(error);}
});
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST - GET EVENTS FROM STORE
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
const employee = Employee.fromEvents(employeeId, events);
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
return res.status(204).send();
} catch (error) { next(error);}
});
const events = await eventStore.getEventsFor(employeeId);
EVENT SOURCE EVERYTHING!
EVENT
STORE
▸ SINGLE SOURCE OF TRUTH
▸ USED ONLY ON WRITE SIDE
▸ USED DURING READ MODEL REBUILD
EVENT STORE
EVENT SOURCE EVERYTHING!
{
type: ‘EMPLOYEE_EMPLOYED’,
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 12:23:34’,
payload: {
firstName: ‘Adam’,
lastName: ‘Polak’,
occupation: ‘Head of Node.js’
}
}
EVENTS
{
type: ‘VACATION_REQUEST_ISSUED’,
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 13:23:34’,
payload: {
id: request-uuid,
from: ‘2018-10-01’,
to: ‘2018-11-01’,
days: 24
}
}
{
type: ‘EMPLOYEE_ASSIGNED’
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 14:23:34’,
payload: {
projectId: project-uuid
}
}
EVENT SOURCE EVERYTHING!
▸ EVENT STORE
▸ MONGODB
▸ POSTGRESQL
POSSIBLE SOLUTIONS
EVENT SOURCE EVERYTHING!
class EventStoreRepository {
constructor(db, upcasters) { … }
async saveStream(events) { … }
getAllEvents() { … }
getEventsFor(aggregateId) {
return this.eventsCollection.find({ aggregateId })
.sort({ "payload.createDate": 1 })
.then(events => events.map(event => this.upcasters.upcast(event)));
}
}
REPOSITORY
EVENT SOURCE EVERYTHING!
class EventStoreRepository {
constructor(db, upcasters) { … }
async saveStream(events) { … }
getAllEvents() { … }
}
REPOSITORY
getEventsFor(aggregateId) {
return this.eventsCollection.find({ aggregateId })
.sort({ "payload.createDate": 1 })
.then(events => events.map(event => this.upcasters.upcast(event)));
}
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST - REBUILD AGGREGATE FROM EVENTS
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
a await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
return res.status(204).send();
} catch (error) { next(error);}
});
const events = await eventStore.getEventsFor(employeeId);
const employee = Employee.fromEvents(employeeId, events);
EVENT SOURCE EVERYTHING!
AGGREGATE ROOT
class Employee {
constructor(id) {
this.id = id;
this.recentEvents = [];
}
static fromEvents(aggregateId, events) { … }
issueVacationRequest(from, to, comment) { … }
apply(event) {…}
getRecentEvents() { … }
}
EVENT SOURCE EVERYTHING!
AGGREGATE ROOT - REBUILD FROM EVENTS
class Employee {
constructor(id) {
this.id = id;
this.recentEvents = [];
}
issueVacationRequest(from, to, comment) { … }
apply(event) {…}
getRecentEvents() { … }
}
static fromEvents(aggregateId, events) { … }
EVENT SOURCE EVERYTHING!
AGGREGATE ROOT - REBUILD FROM EVENTS
{
type: ‘EMPLOYEE_EMPLOYED’,
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 12:23:34’,
payload: {
firstName: ‘Adam’,
lastName: ‘Polak’,
occupation: ‘JS Developer’
}
}
{
type: ‘EMPLOYEE_OCCUPATION_CHANGED’,
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 13:23:34’,
payload: {
occupation: ‘Head of Node.js’
}
}
{
id: my-uuid,
firstName: ‘Adam’,
lastName: ‘Polak’,
occupation: ‘JS Developer’
}
{
id: my-uuid,
firstName: ‘Adam’,
lastName: ‘Polak’,
occupation: ‘Head of Node.js’
}
EVENT SOURCE EVERYTHING!
static fromEvents(id, events) {
return events.reduce((employee, event) => {
employee.apply(event);
return employee;
}, new Employee(id))
}
AGGREGATE ROOT - REBUILD FROM EVENTS
EVENT SOURCE EVERYTHING!
apply(event) {
if (event.type === ‘EMPLOYEE_EMPLOYED’) {
this.firstName = event.payload.firstName;
this.lastName = event.payload.lastName;
this.occupation = event.payload.occupation;
} else {
throw new AppException(`Missing handler for event with type ${event.type}`);
}
}
AGGREGATE ROOT - APPLY EVENTS
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST - PERFORM ACTION ON DOMAIN MODEL
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
return res.status(204).send();
} catch (error) { next(error);}
});
const events = await eventStore.getEventsFor(employeeId);
const employee = Employee.fromEvents(employeeId, events);
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST
issueVacationRequest(from, to, comment) {
const requestId = uuid.v4();
const days = VacationRequest.calculateDays(from, to);
if (this.vacationDays.availableDays < days) {
throw new Error(`Not enough free days to take vacation for '${days}' days`);
}
const employeeIssuedVacationRequestEvent = employeeIssuedVacationRequest.create( … );
this.recentEvents.push(employeeIssuedVacationRequestEvent);
this.apply(employeeIssuedVacationRequestEvent);
}
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST - EMIT EVENT FROM AGGREGATE
issueVacationRequest(from, to, comment) {
const requestId = uuid.v4();
const days = VacationRequest.calculateDays(from, to);
if (this.vacationDays.availableDays < days) {
throw new Error(`Not enough free days to take vacation for '${days}' days`);
}
}
const employeeIssuedVacationRequestEvent = employeeIssuedVacationRequest.create( … );
this.recentEvents.push(employeeIssuedVacationRequestEvent);
this.apply(employeeIssuedVacationRequestEvent);
EVENT SOURCE EVERYTHING!
ISSUE VACATION REQUEST - SAVE RECENT EVENTS
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
return res.status(204).send();
} catch (error) { next(error);}
});
const events = await eventStore.getEventsFor(employeeId);
const employee = Employee.fromEvents(employeeId, events);
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
EVENT SOURCE EVERYTHING!
getRecentEvents() {
const events = [...this.recentEvents];
this.recentEvents = [];
return events;
}
AGGREGATE ROOT - GET RECENT EVENTS
EVENT SOURCE EVERYTHING!
PASS EVENTS TO
EVENT STORE
class EventStoreRepository {
constructor(db, upcasters) { … }
async saveStream(events) {
await this.eventsCollection.insert(events);
return events;
}
getAllEvents() { … }
getEventsForm(aggregateId) { … }
}
REPOSITORY - SAVE STREAM
EVENT SOURCE EVERYTHING!
class EventStoreRepository {
constructor(db, upcasters) { … }
async saveStream(events) {
await this.eventsCollection.insert(events);
}
getAllEvents() { … }
getEventsForm(aggregateId) { … }
}
REPOSITORY - SAVE STREAM
EVENT SOURCE EVERYTHING!
// SAVE IN SINGLE “TRANSACTION”!
async saveStream(events) {
await this.eventsCollection.insert(events);
return events;
}
| id | aggregateId | aggregateVersion | type | createDate | payload | version |
| 10 | 1 | 10 | EMPLOYEE_UPDATED | 2018-01-11 | { ………………………………… } | 4 |
| 9 | 1 | 9 | EMPLOYEE_UPDATED | 2018-01-10 | { ………………………………… } | 4 |
| 8 | 1 | 8 | EMPLOYEE_UPDATED | 2018-01-09 | { ………………………………… } | 3 |
| 7 | 1 | 7 | EMPLOYEE_UPDATED | 2018-01-08 | { ………………………………… } | 3 |
| 6 | 1 | 6 | EMPLOYEE_UPDATED | 2018-01-07 | { ………………………………… } | 2 |
| 5 | 1 | 5 | EMPLOYEE_UPDATED | 2018-01-06 | { ………………………………… } | 2 |
| 4 | 1 | 4 | EMPLOYEE_UPDATED | 2018-01-05 | { ………………………………… } | 1 |
| 3 | 1 | 3 | EMPLOYEE_UPDATED | 2018-01-04 | { ………………………………… } | 1 |
| 2 | 1 | 2 | EMPLOYEE_UPDATED | 2018-01-03 | { ………………………………… } | 1 |
| 1 | 1 | 1 | EMPLOYEE_EMPLOYED | 2018-01-02 | { ………………………………… } | 1 |
EVENT STORE - PERSISTED EVENTS
EVENT SOURCE EVERYTHING!
▸ HOW TO PAGINATE?
▸ HOW TO DISPLAY SINGLE EMPLOYEE?
▸ HOW TO DISPLAY FILTER EMPLOYEES?
▸ HOW TO DO BASIC UI OPERATIONS?
EVENT SOURCE EVERYTHING!
PROBLEMS
EVENT
BUS
PROJECTION STORAGE UI
EVENT SOURCE EVERYTHING!
READ SIDE
EVENT
BUS
EVENT SOURCE EVERYTHING!
READ SIDE
EVENT BUS
router.post('/:id/vacations', celebrate({
body: Joi.object().keys({
from: Joi.date().required(),
to: Joi.date().required(),
comment: Joi.string().allow(null)
})
}), async (req, res, next) => {
const employeeId = req.params.id;
try {
return res.status(204).send();
} catch (error) { next(error);}
});
const events = await eventStore.getEventsFor(employeeId);
const employee = Employee.fromEvents(employeeId, events);
employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment);
await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events));
EVENT SOURCE EVERYTHING!
EVENT BUS
EVENT BUS
PROJECTORS
MAILER
HANDLER
EVENT SOURCE EVERYTHING!
{
type: ‘EMPLOYEE_EMPLOYED’,
aggregateId: my-uuid,
version: 1,
createDate: ‘2018-01-01 12:23:34’,
payload: {
firstName: ‘Adam’,
lastName: ‘Polak’,
occupation: ‘Head of Node.js’
}
}
ANY MESSAGING
SYSTEM
EVENTS BUS
class EventBus {
constructor() { this.subscribers = []; }
subscribe(event, handler) {
if (typeof this.subscribers[`${event}`] === 'undefined') {
this.subscribers[`${event}`] = [];
}
this.subscribers[`${event}`].push(handler);
}
publish(events) {
events.forEach(event => {
if (this.subscribers[`${event.type}`]) {
this.subscribers[`${event.type}`].forEach((handler) => handler(event));
}
}
}
}
EVENT SOURCE EVERYTHING!
EVENT
BUS
PROJECTION STORAGE UI
EVENT SOURCE EVERYTHING!
PROJECTOR
const handler = connection => event => {
if (event.type === eventTypes.EMPLOYEE_EMPLOYED) {
return connection.collection(‘employees’).insert({ … });
}
if (event.type === eventTypes.EMPLOYEE_OCCUPATION_CHANGED) {
return connection.collection(‘employees’).update({ … });
}
};
EVENT SOURCE EVERYTHING!
UI CHANGED?
REBUILD PROJECTION
FROM SCRATCH
PROJECTION REBUILD
app.then(async (db) => {
const eventStoreRepository = new EventStoreRepository(db, new Upcasters());
const projector = activeTasksProjector.rebuilder(db);
const events = await eventStoreRepository.getAll();
projector(events).then(() => process.exit(0));
});
EVENT SOURCE EVERYTHING!
▸ IT IS CQRS
▸ REBUILD AGGREGATE FROM PREVIOUS EVENTS
▸ EVERY OPERATION EMITS DOMAIN EVENTS
▸ STORE EVENTS IN EVENT STORE
▸ PUBLISH THEM THROUGH EVENT BUS
▸ BUILD PROJECTIONS FROM PUBLISHED EVENTS
▸ REBUILD PROJECTIONS IF NECESSARY
EVENT SOURCE EVERYTHING!
SUMMARY
CHALLENGES!
VERSIONING
▸ REBUILD EVENT STORE (NOT A GOOD IDEA)
▸ UPCAST AT RUNTIME
EVENT SOURCE EVERYTHING!
VERSIONING
UPCASTING
UPCASTING
{
type: ‘REQUEST_APPROVED’,
aggregateId: 1,
version: 1,
createDate: ‘2018-01-01 12:23:34’,
payload: {
id: ‘some-request-id’
}
}
{
type: ‘REQUEST_APPROVED’,
aggregateId: 1,
version: 2,
createDate: ‘2018-01-01 12:23:34’,
payload: {
id: ‘some-request-id’,
approver: ‘Adam Polak’
}
}
EVENT SOURCE EVERYTHING!
UPCASTING
{
type: ‘REQUEST_APPROVED’,
aggregateId: 1,
version: 1,
createDate: ‘2018-01-01 12:23:34’,
payload: {
id: ‘some-request-id’
}
}
{
type: ‘REQUEST_APPROVED’,
aggregateId: 1,
version: 2,
createDate: ‘2018-01-01 12:23:34’,
payload: {
id: ‘some-request-id’,
approver: ‘Adam Polak’
}
}
EVENT SOURCE EVERYTHING!
UPCASTER
class EventStoreRepository {
constructor(db, upcasters) { … }
async saveStream(events) { … }
getAllEvents() { … }
}
getEventsFor(aggregateId) {
return this.eventsCollection.find({ aggregateId })
.sort({ "payload.createDate": 1 })
.then(events => events.map(event => this.upcasters.upcast(event)));
}
EVENT SOURCE EVERYTHING!
EVENT STORE REPOSITORY
UPCASTER
const eventTypes = require('../employee/events/types');
module.exports = {
canUpcast: (event) => event.version === 1 && event.type === eventTypes.VACATION_REQUEST_APPROVED,
upcast: (event) => ({
...event,
version: 2,
payload: {
...event.payload,
approver: ‘hr-member‘
}
})
};
EVENT SOURCE EVERYTHING!
PERFORMANCE
MORE EVENTS
=
PERFORMANCE LOSS

▸ AVOID LONG LIVING AGGREGATES
▸ SNAPSHOTS
EVENT SOURCE EVERYTHING!
PERFORMANCE IMPROVEMENTS
SNAPSHOTS
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 13:23:34’,
payload: {
description: ‘new-description’
}
}
{
type: ‘TASK_SNAPSHOT,
aggregateId: 1,
aggregateVersion: 2,
version: 1,
createDate: ‘2018-01-01 14:23:34’,
payload: {
title: ‘some-title’,
description: ‘new-description’
}
}
EVENT SOURCE EVERYTHING!
GET SNAPSHOT
APPLY NEWER
EVENTS
QUESTIONS ?
THANK YOU

Event source everything!

  • 1.
  • 2.
    EVENT SOURCE EVERYTHING! AGENDA ▸EVENT SOURCING BASICS ▸ WHAT ABOUT STORE? ▸ HOW TO HANDLE EVENTS? ▸ VERSIONING ▸ PERFORMANCE ▸ QUESTIONS
  • 3.
    ADAM POLAK HEAD OFNODE.JS ADAM.POLAK@THESOFTWAREHOUSE.PL POLAK.ADAM1
  • 4.
  • 5.
    ▸ CREATE EMPLOYEE ▸ISSUE VACATION REQUEST ▸ APPROVE/DECLINE VACATION REQUEST HR MANAGEMENT EVENT SOURCE EVERYTHING!
  • 6.
  • 7.
    EMPLOYEE class Employee { constructor(id,firstName, lastName, vacationRequests) { this._id = id; this._firstName = firstName; this._lastName = lastName; this._vacationRequests = vacationRequests; } issueVacationRequest(from, to, comment) { // do business logic and domain validation here then return vacation req or event} } EVENT SOURCE EVERYTHING!
  • 8.
    ISSUE VACATION REQUEST constvacationRequest = employee.issueValidationRequest(‘2018-10-01’, ‘2018-11-01’, ‘Japan, here I come!’); vacationRequestsRepository.persist(vacationRequest); // map vacation request from domain model to db model const employee = employeeRepository.findById(id); // map from db model to domain model EVENT SOURCE EVERYTHING! | id | employee_id | from | to | comment | create_date | update_date | | 1 | some-uuid | 2018-10-01 | 2018-11-01 | Japan, here I come! | 2018-01-11 | 2018-01-11 |
  • 9.
  • 10.
  • 11.
    ▸ EMPLOYEE EMPLOYED ▸VACATION REQUEST ISSUED ▸ VACATION REQUEST APPROVED ▸ VACATION REQUEST DECLINED ▸ EMPLOYEE ASSIGNED TO PROJECT ▸ EMPLOYEE DETAILS UPDATED EVENTS EVENT SOURCE EVERYTHING!
  • 12.
  • 13.
  • 14.
  • 15.
    REBUILD FROM EVENTSCOMMAND OPERATIONS WRITESIDE EMIT EVENTS EVENT STORE EVENT SOURCE EVERYTHING!
  • 16.
  • 17.
  • 18.
    ISSUE VACATION REQUEST router.post('/:id/vacations',celebrate({ body: Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { const events = await eventStore.getEventsFor(employeeId); const employee = Employee.fromEvents(employeeId, events); employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); return res.status(204).send(); } catch (error) { next(error);} }); EVENT SOURCE EVERYTHING!
  • 19.
    ISSUE VACATION REQUEST- GET EVENTS FROM STORE router.post('/:id/vacations', celebrate({ body: Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { const employee = Employee.fromEvents(employeeId, events); employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); return res.status(204).send(); } catch (error) { next(error);} }); const events = await eventStore.getEventsFor(employeeId); EVENT SOURCE EVERYTHING!
  • 20.
  • 21.
    ▸ SINGLE SOURCEOF TRUTH ▸ USED ONLY ON WRITE SIDE ▸ USED DURING READ MODEL REBUILD EVENT STORE EVENT SOURCE EVERYTHING!
  • 22.
    { type: ‘EMPLOYEE_EMPLOYED’, aggregateId: my-uuid, version:1, createDate: ‘2018-01-01 12:23:34’, payload: { firstName: ‘Adam’, lastName: ‘Polak’, occupation: ‘Head of Node.js’ } } EVENTS { type: ‘VACATION_REQUEST_ISSUED’, aggregateId: my-uuid, version: 1, createDate: ‘2018-01-01 13:23:34’, payload: { id: request-uuid, from: ‘2018-10-01’, to: ‘2018-11-01’, days: 24 } } { type: ‘EMPLOYEE_ASSIGNED’ aggregateId: my-uuid, version: 1, createDate: ‘2018-01-01 14:23:34’, payload: { projectId: project-uuid } } EVENT SOURCE EVERYTHING!
  • 23.
    ▸ EVENT STORE ▸MONGODB ▸ POSTGRESQL POSSIBLE SOLUTIONS EVENT SOURCE EVERYTHING!
  • 24.
    class EventStoreRepository { constructor(db,upcasters) { … } async saveStream(events) { … } getAllEvents() { … } getEventsFor(aggregateId) { return this.eventsCollection.find({ aggregateId }) .sort({ "payload.createDate": 1 }) .then(events => events.map(event => this.upcasters.upcast(event))); } } REPOSITORY EVENT SOURCE EVERYTHING!
  • 25.
    class EventStoreRepository { constructor(db,upcasters) { … } async saveStream(events) { … } getAllEvents() { … } } REPOSITORY getEventsFor(aggregateId) { return this.eventsCollection.find({ aggregateId }) .sort({ "payload.createDate": 1 }) .then(events => events.map(event => this.upcasters.upcast(event))); } EVENT SOURCE EVERYTHING!
  • 26.
    ISSUE VACATION REQUEST- REBUILD AGGREGATE FROM EVENTS router.post('/:id/vacations', celebrate({ body: Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); a await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); return res.status(204).send(); } catch (error) { next(error);} }); const events = await eventStore.getEventsFor(employeeId); const employee = Employee.fromEvents(employeeId, events); EVENT SOURCE EVERYTHING!
  • 27.
    AGGREGATE ROOT class Employee{ constructor(id) { this.id = id; this.recentEvents = []; } static fromEvents(aggregateId, events) { … } issueVacationRequest(from, to, comment) { … } apply(event) {…} getRecentEvents() { … } } EVENT SOURCE EVERYTHING!
  • 28.
    AGGREGATE ROOT -REBUILD FROM EVENTS class Employee { constructor(id) { this.id = id; this.recentEvents = []; } issueVacationRequest(from, to, comment) { … } apply(event) {…} getRecentEvents() { … } } static fromEvents(aggregateId, events) { … } EVENT SOURCE EVERYTHING!
  • 29.
    AGGREGATE ROOT -REBUILD FROM EVENTS { type: ‘EMPLOYEE_EMPLOYED’, aggregateId: my-uuid, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { firstName: ‘Adam’, lastName: ‘Polak’, occupation: ‘JS Developer’ } } { type: ‘EMPLOYEE_OCCUPATION_CHANGED’, aggregateId: my-uuid, version: 1, createDate: ‘2018-01-01 13:23:34’, payload: { occupation: ‘Head of Node.js’ } } { id: my-uuid, firstName: ‘Adam’, lastName: ‘Polak’, occupation: ‘JS Developer’ } { id: my-uuid, firstName: ‘Adam’, lastName: ‘Polak’, occupation: ‘Head of Node.js’ } EVENT SOURCE EVERYTHING!
  • 30.
    static fromEvents(id, events){ return events.reduce((employee, event) => { employee.apply(event); return employee; }, new Employee(id)) } AGGREGATE ROOT - REBUILD FROM EVENTS EVENT SOURCE EVERYTHING!
  • 31.
    apply(event) { if (event.type=== ‘EMPLOYEE_EMPLOYED’) { this.firstName = event.payload.firstName; this.lastName = event.payload.lastName; this.occupation = event.payload.occupation; } else { throw new AppException(`Missing handler for event with type ${event.type}`); } } AGGREGATE ROOT - APPLY EVENTS EVENT SOURCE EVERYTHING!
  • 32.
    ISSUE VACATION REQUEST- PERFORM ACTION ON DOMAIN MODEL router.post('/:id/vacations', celebrate({ body: Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); return res.status(204).send(); } catch (error) { next(error);} }); const events = await eventStore.getEventsFor(employeeId); const employee = Employee.fromEvents(employeeId, events); employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); EVENT SOURCE EVERYTHING!
  • 33.
    ISSUE VACATION REQUEST issueVacationRequest(from,to, comment) { const requestId = uuid.v4(); const days = VacationRequest.calculateDays(from, to); if (this.vacationDays.availableDays < days) { throw new Error(`Not enough free days to take vacation for '${days}' days`); } const employeeIssuedVacationRequestEvent = employeeIssuedVacationRequest.create( … ); this.recentEvents.push(employeeIssuedVacationRequestEvent); this.apply(employeeIssuedVacationRequestEvent); } EVENT SOURCE EVERYTHING!
  • 34.
    ISSUE VACATION REQUEST- EMIT EVENT FROM AGGREGATE issueVacationRequest(from, to, comment) { const requestId = uuid.v4(); const days = VacationRequest.calculateDays(from, to); if (this.vacationDays.availableDays < days) { throw new Error(`Not enough free days to take vacation for '${days}' days`); } } const employeeIssuedVacationRequestEvent = employeeIssuedVacationRequest.create( … ); this.recentEvents.push(employeeIssuedVacationRequestEvent); this.apply(employeeIssuedVacationRequestEvent); EVENT SOURCE EVERYTHING!
  • 35.
    ISSUE VACATION REQUEST- SAVE RECENT EVENTS router.post('/:id/vacations', celebrate({ body: Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { return res.status(204).send(); } catch (error) { next(error);} }); const events = await eventStore.getEventsFor(employeeId); const employee = Employee.fromEvents(employeeId, events); employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); EVENT SOURCE EVERYTHING!
  • 36.
    getRecentEvents() { const events= [...this.recentEvents]; this.recentEvents = []; return events; } AGGREGATE ROOT - GET RECENT EVENTS EVENT SOURCE EVERYTHING!
  • 37.
  • 38.
    class EventStoreRepository { constructor(db,upcasters) { … } async saveStream(events) { await this.eventsCollection.insert(events); return events; } getAllEvents() { … } getEventsForm(aggregateId) { … } } REPOSITORY - SAVE STREAM EVENT SOURCE EVERYTHING!
  • 39.
    class EventStoreRepository { constructor(db,upcasters) { … } async saveStream(events) { await this.eventsCollection.insert(events); } getAllEvents() { … } getEventsForm(aggregateId) { … } } REPOSITORY - SAVE STREAM EVENT SOURCE EVERYTHING! // SAVE IN SINGLE “TRANSACTION”! async saveStream(events) { await this.eventsCollection.insert(events); return events; }
  • 40.
    | id |aggregateId | aggregateVersion | type | createDate | payload | version | | 10 | 1 | 10 | EMPLOYEE_UPDATED | 2018-01-11 | { ………………………………… } | 4 | | 9 | 1 | 9 | EMPLOYEE_UPDATED | 2018-01-10 | { ………………………………… } | 4 | | 8 | 1 | 8 | EMPLOYEE_UPDATED | 2018-01-09 | { ………………………………… } | 3 | | 7 | 1 | 7 | EMPLOYEE_UPDATED | 2018-01-08 | { ………………………………… } | 3 | | 6 | 1 | 6 | EMPLOYEE_UPDATED | 2018-01-07 | { ………………………………… } | 2 | | 5 | 1 | 5 | EMPLOYEE_UPDATED | 2018-01-06 | { ………………………………… } | 2 | | 4 | 1 | 4 | EMPLOYEE_UPDATED | 2018-01-05 | { ………………………………… } | 1 | | 3 | 1 | 3 | EMPLOYEE_UPDATED | 2018-01-04 | { ………………………………… } | 1 | | 2 | 1 | 2 | EMPLOYEE_UPDATED | 2018-01-03 | { ………………………………… } | 1 | | 1 | 1 | 1 | EMPLOYEE_EMPLOYED | 2018-01-02 | { ………………………………… } | 1 | EVENT STORE - PERSISTED EVENTS EVENT SOURCE EVERYTHING!
  • 41.
    ▸ HOW TOPAGINATE? ▸ HOW TO DISPLAY SINGLE EMPLOYEE? ▸ HOW TO DISPLAY FILTER EMPLOYEES? ▸ HOW TO DO BASIC UI OPERATIONS? EVENT SOURCE EVERYTHING! PROBLEMS
  • 42.
    EVENT BUS PROJECTION STORAGE UI EVENTSOURCE EVERYTHING! READ SIDE
  • 43.
  • 44.
    EVENT BUS router.post('/:id/vacations', celebrate({ body:Joi.object().keys({ from: Joi.date().required(), to: Joi.date().required(), comment: Joi.string().allow(null) }) }), async (req, res, next) => { const employeeId = req.params.id; try { return res.status(204).send(); } catch (error) { next(error);} }); const events = await eventStore.getEventsFor(employeeId); const employee = Employee.fromEvents(employeeId, events); employee.issueVacationRequest(req.body.from, req.body.to, req.body.comment); await eventStore.saveStream(employee.getRecentEvents()).then(events => eventBus.publish(events)); EVENT SOURCE EVERYTHING!
  • 45.
    EVENT BUS EVENT BUS PROJECTORS MAILER HANDLER EVENTSOURCE EVERYTHING! { type: ‘EMPLOYEE_EMPLOYED’, aggregateId: my-uuid, version: 1, createDate: ‘2018-01-01 12:23:34’, payload: { firstName: ‘Adam’, lastName: ‘Polak’, occupation: ‘Head of Node.js’ } }
  • 46.
  • 47.
    EVENTS BUS class EventBus{ constructor() { this.subscribers = []; } subscribe(event, handler) { if (typeof this.subscribers[`${event}`] === 'undefined') { this.subscribers[`${event}`] = []; } this.subscribers[`${event}`].push(handler); } publish(events) { events.forEach(event => { if (this.subscribers[`${event.type}`]) { this.subscribers[`${event.type}`].forEach((handler) => handler(event)); } } } } EVENT SOURCE EVERYTHING!
  • 48.
  • 49.
    PROJECTOR const handler =connection => event => { if (event.type === eventTypes.EMPLOYEE_EMPLOYED) { return connection.collection(‘employees’).insert({ … }); } if (event.type === eventTypes.EMPLOYEE_OCCUPATION_CHANGED) { return connection.collection(‘employees’).update({ … }); } }; EVENT SOURCE EVERYTHING!
  • 50.
  • 51.
  • 52.
    PROJECTION REBUILD app.then(async (db)=> { const eventStoreRepository = new EventStoreRepository(db, new Upcasters()); const projector = activeTasksProjector.rebuilder(db); const events = await eventStoreRepository.getAll(); projector(events).then(() => process.exit(0)); }); EVENT SOURCE EVERYTHING!
  • 53.
    ▸ IT ISCQRS ▸ REBUILD AGGREGATE FROM PREVIOUS EVENTS ▸ EVERY OPERATION EMITS DOMAIN EVENTS ▸ STORE EVENTS IN EVENT STORE ▸ PUBLISH THEM THROUGH EVENT BUS ▸ BUILD PROJECTIONS FROM PUBLISHED EVENTS ▸ REBUILD PROJECTIONS IF NECESSARY EVENT SOURCE EVERYTHING! SUMMARY
  • 54.
  • 55.
  • 56.
    ▸ REBUILD EVENTSTORE (NOT A GOOD IDEA) ▸ UPCAST AT RUNTIME EVENT SOURCE EVERYTHING! VERSIONING
  • 57.
  • 58.
    UPCASTING { type: ‘REQUEST_APPROVED’, aggregateId: 1, version:1, createDate: ‘2018-01-01 12:23:34’, payload: { id: ‘some-request-id’ } } { type: ‘REQUEST_APPROVED’, aggregateId: 1, version: 2, createDate: ‘2018-01-01 12:23:34’, payload: { id: ‘some-request-id’, approver: ‘Adam Polak’ } } EVENT SOURCE EVERYTHING!
  • 59.
    UPCASTING { type: ‘REQUEST_APPROVED’, aggregateId: 1, version:1, createDate: ‘2018-01-01 12:23:34’, payload: { id: ‘some-request-id’ } } { type: ‘REQUEST_APPROVED’, aggregateId: 1, version: 2, createDate: ‘2018-01-01 12:23:34’, payload: { id: ‘some-request-id’, approver: ‘Adam Polak’ } } EVENT SOURCE EVERYTHING! UPCASTER
  • 60.
    class EventStoreRepository { constructor(db,upcasters) { … } async saveStream(events) { … } getAllEvents() { … } } getEventsFor(aggregateId) { return this.eventsCollection.find({ aggregateId }) .sort({ "payload.createDate": 1 }) .then(events => events.map(event => this.upcasters.upcast(event))); } EVENT SOURCE EVERYTHING! EVENT STORE REPOSITORY
  • 61.
    UPCASTER const eventTypes =require('../employee/events/types'); module.exports = { canUpcast: (event) => event.version === 1 && event.type === eventTypes.VACATION_REQUEST_APPROVED, upcast: (event) => ({ ...event, version: 2, payload: { ...event.payload, approver: ‘hr-member‘ } }) }; EVENT SOURCE EVERYTHING!
  • 62.
  • 63.
  • 64.
    ▸ AVOID LONGLIVING AGGREGATES ▸ SNAPSHOTS EVENT SOURCE EVERYTHING! PERFORMANCE IMPROVEMENTS
  • 65.
  • 66.
    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 13:23:34’, payload: { description: ‘new-description’ } } { type: ‘TASK_SNAPSHOT, aggregateId: 1, aggregateVersion: 2, version: 1, createDate: ‘2018-01-01 14:23:34’, payload: { title: ‘some-title’, description: ‘new-description’ } } EVENT SOURCE EVERYTHING!
  • 67.
  • 68.
  • 69.
  • 70.