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

Building Event-Driven Microservices with Event Sourcing and CQRS

Lidan
June 14, 2018

Building Event-Driven Microservices with Event Sourcing and CQRS

Most systems today store only the current state of their business entities. However, we can look at the current state through a different lens- as a derivative of previous behaviors, and store those behaviors as a sequence of events instead of the current state of the entity. The current state will be derived by replaying the events.

Event sourcing is a great way to implement event-driven applications, and it’s often combined with CQRS (Command Query Responsibility Segregation) which is a key part of an architecture based on event sourcing.

In this talk I will cover the principles of event sourcing pattern, how to model your data and implement business logic using this pattern and how to improve your system through the segregation of reads and writes with specialized read models.

You will also learn how those concepts fit in with event-driven micro services through a real life example- an invoices management app we built at Wix.

Lidan

June 14, 2018
Tweet

More Decks by Lidan

Other Decks in Technology

Transcript

  1. Lidan Hifi Team Lead, Wix User Notifications Platform Israel Tel

    Aviv Be’er Sheva Lithuania Vilnius Ukraine Dnipropetrovsk Kiev
  2. Create Invoice id: 809f142f-afba-4cad-93b5-d26b3ae7a021 customer: { name: “lidanh”, id: …

    } currency: USD line items: [ … ] COMMAND Named with an imperative verb Express the user’s intent POST /invoices/a33b7d1f More meaningful than HTTP methods CreateInvoice vs.
  3. Create Invoice id: 809f142f-afba-4cad-93b5-d26b3ae7a021 customer: { name: “lidanh”, id: …

    } currency: USD line items: [ … ] COMMAND EVENTS 1 InvoiceCreated 2 InvoiceCustomerSet: lidanh 3 InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT 5 LineItemAdded: { … } 6 LineItemAdded: { … } Named with a past-participle verb Events are immutable Prefer fine-grained events Named with an imperative verb Express the user’s intent POST /invoices/a33b7d1f More meaningful than HTTP methods CreateInvoice vs. time
  4. Create Invoice id: 809f142f-afba-4cad-93b5-d26b3ae7a021 customer: { name: “lidanh”, id: …

    } currency: USD line items: [ … ] COMMAND EVENTS 1 InvoiceCreated 2 InvoiceCustomerSet: lidanh 3 InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT 5 LineItemAdded: { … } 6 LineItemAdded: { … } DB INSERT INTO invoice_events time
  5. Regenerating State 1 InvoiceCreated 2 InvoiceCustomerSet: lidanh 3 InvoiceCurrencySet: USD

    4 InvoiceStatusSet: DRAFT 5 LineItemAdded: { … } DB AGGREGATE EVENTS
  6. 10 % 90 % READS WRITES Easier to Scale Flexible

    View Models Normalized Form Validations
  7. 10 % 90 % READS WRITES Easier to Scale Flexible

    View Models Normalized Form Validations SELECT A.SD1, B.ED1 FROM (SELECT SD1, ROW_NUMBER() OVER (ORDER BY SD1) AS RN1 FROM (SELECT T1.Start_Date AS SD1, T2.Start_Date AS SD2 FROM (SELECT * FROM Projects ORDER BY Start_Date) T1 LEFT JOIN (SELECT * FROM Projects ORDER BY Start_Date) T2 ON T1.Start_Date=(T2.Start_Date+1) ORDER BY T1.Start_Date) WHERE SD2 IS NULL) A INNER JOIN (SELECT ED1, ROW_NUMBER() OVER (ORDER BY ED1) AS RN2 FROM (SELECT T1.End_Date AS ED1, T2.Start_Date AS SD2 FROM (SELECT * FROM Projects ORDER BY Start_Date) T1 LEFT JOIN (SELECT * FROM Projects ORDER BY Start_Date) T2 ON T1.End_Date=(T2.Start_Date) ORDER BY T1.Start_Date) WHERE SD2 IS NULL) B ON A.RN1=B.RN2 ORDER BY (B.ED1-A.SD1), A.SD1;
  8. 10 % 90 % READS WRITES Easier to Scale Flexible

    View Models Normalized Form Validations SELECT A.SD1, B.ED1 FROM (SELECT SD1, ROW_NUMBER() OVER (ORDER BY SD1) AS RN1 FROM (SELECT T1.Start_Date AS SD1, T2.Start_Date AS SD2 FROM (SELECT * FROM Projects ORDER BY Start_Date) T1 LEFT JOIN (SELECT * FROM Projects ORDER BY Start_Date) T2 ON T1.Start_Date=(T2.Start_Date+1) ORDER BY T1.Start_Date) WHERE SD2 IS NULL) A INNER JOIN (SELECT ED1, ROW_NUMBER() OVER (ORDER BY ED1) AS RN2 FROM (SELECT T1.End_Date AS ED1, T2.Start_Date AS SD2 FROM (SELECT * FROM Projects ORDER BY Start_Date) T1 LEFT JOIN (SELECT * FROM Projects ORDER BY Start_Date) T2 ON T1.End_Date=(T2.Start_Date) ORDER BY T1.Start_Date) WHERE SD2 IS NULL) B ON A.RN1=B.RN2 ORDER BY (B.ED1-A.SD1), A.SD1;
  9. Let’s CQRSify /invoices/:id BUSINESS LOGIC DATABASES AND OTHER SERVICES Domain

    Model DAO WEB SERVICE /invoices/:id Command Logic DB Read Optimized Views Domain Model DAO Query Logic POST GET
  10. Let’s CQRSify /invoices/:id BUSINESS LOGIC DATABASES AND OTHER SERVICES Domain

    Model DAO WEB SERVICE /invoices/:id Command Logic DB Read Optimized Views Domain Model DAO Query Logic Inconsistency Window POST GET
  11. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Analytics Emails Command Handler Event Bus /invoice/:id Read Optimized Views
  12. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Command Handler Event Bus { command: “SendInvoiceToCustomer", aggregateId: "809f142f-afba-4cad-93b5-d26b3ae7a021", payload: { subject:"New Invoice from Lidan Ltd.", message: "You received an invoice from Lidan Ltd." } }
  13. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Command Handler Event Bus
  14. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Command Handler Event Bus
  15. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Command Handler Event Bus
  16. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Command Handler Event Bus
  17. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Analytics Emails Command Handler Event Bus Read Optimized Views
  18. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Analytics Emails Command Handler Event Bus /invoice/:id Read Optimized Views
  19. WEB SERVICE /execute Command Decoder COMMAND HANDLING DATABASES AND EVENT

    HANDLERS Events Store Analytics Emails Command Handler Event Bus /invoice/:id Read Optimized Views
  20. THINGS WE LIKE Audit Log “for free” Debugging Tool Historic

    State - Time Travel 1 Log, Multiple Schemas
  21. Handling Consistency Strong vs. Eventually On-the-fly views Synchronous Writes Command

    Handler Read Optimized View Read Optimized View Read Optimized View
  22. Handling Consistency Strong vs. Eventually Events Store Read Optimized Views

    version? id: 1 version: 10 id: 1 version: 15 On-the-fly views Synchronous Writes Delta Updates
  23. Handling Consistency Strong vs. Eventually Events Store Read Optimized Views

    version? id: 1 version: 10 id: 1 version: 15 update On-the-fly views Synchronous Writes Delta Updates
  24. Handling Validations Single Command- simple input validation https://github.com/wix/accord Aggregate Validation-

    can I apply this command? CreationCommand, DeletionCommand case class CreateLabel(...) extends LabelCommand with CreationCommand case class DeleteLabel(...) extends LabelCommand with DeletionCommand
  25. Handling Validations Single Command- simple input validation https://github.com/wix/accord Aggregate Validation-

    can I apply this command? CreationCommand, DeletionCommand Complex Validation based on domain requirements Validate against the read store Emails per tenant id tenant id email 53d6211d-acfb-468c- bf7d-7245a630b64b [email protected] 53d6211d-acfb-468c- bf7d-7245a630b64b [email protected] 53d6211d-acfb-468c- bf7d-7245a630b64b [email protected]
  26. Concurrent Commands How to Resolve Conflicts Update Invoice Delete Invoice

    Pessimistic Locking An aggregate can only be accessed by one thread at a time EVENTS DB
  27. Concurrent Commands How to Resolve Conflicts Optimistic Locking Expected Version

    < Actual Version ? Update Invoice version: 3 Delete Invoice version: 3 2 InvoiceCustomerSet: lidanh 3 InvoiceCurrencySet: USD
  28. Concurrent Commands How to Resolve Conflicts Optimistic Locking Expected Version

    < Actual Version ? Update Invoice version: 3 Delete Invoice version: 3 2 InvoiceCustomerSet: lidanh 3 InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT ConcurrentModificationException
  29. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution
  30. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution 4 LineItemAdded: { … }
  31. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution 5 LineItemAdded: { … }
  32. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution
  33. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution 4 InvoiceStatusSet: DELETED
  34. Concurrent Commands How to Resolve Conflicts 2 InvoiceCustomerSet: lidanh 3

    InvoiceCurrencySet: USD 4 InvoiceStatusSet: DRAFT Conflict Resolution Concurrent Modification 4 InvoiceStatusSet: DELETED
  35. def load(tenantId: TenantId, aggregateId: AggregateId, revision: Option[AggregateVersion] = None): Option[SnapshotType]

    = { } val events = eventStreamReader.readStream( tenantId.id, aggregateId.id, revision.map(_ => VersionRange(to = revision))) events.lastOption.map { lastEvent => } val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) AggregateSnapshot(aggregate, lastEvent.aggregateVersion, events.head.timestamp, lastEvent.timestamp)
  36. def load(tenantId: TenantId, aggregateId: AggregateId, revision: Option[AggregateVersion] = None): Option[SnapshotType]

    = { } val events = eventStreamReader.readStream( tenantId.id, aggregateId.id, revision.map(_ => VersionRange(to = revision))) events.lastOption.map { lastEvent => } val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) AggregateSnapshot(aggregate, lastEvent.aggregateVersion, events.head.timestamp, lastEvent.timestamp)
  37. def load(tenantId: TenantId, aggregateId: AggregateId, revision: Option[AggregateVersion] = None): Option[SnapshotType]

    = { } val events = eventStreamReader.readStream( tenantId.id, aggregateId.id, revision.map(_ => VersionRange(to = revision))) events.lastOption.map { lastEvent => } val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) AggregateSnapshot(aggregate, lastEvent.aggregateVersion, events.head.timestamp, lastEvent.timestamp)
  38. def load(tenantId: TenantId, aggregateId: AggregateId, revision: Option[AggregateVersion] = None): Option[SnapshotType]

    = { } val events = eventStreamReader.readStream( tenantId.id, aggregateId.id, revision.map(_ => VersionRange(to = revision))) events.lastOption.map { lastEvent => } val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) AggregateSnapshot(aggregate, lastEvent.aggregateVersion, events.head.timestamp, lastEvent.timestamp)
  39. def load(tenantId: TenantId, aggregateId: AggregateId, revision: Option[AggregateVersion] = None): Option[SnapshotType]

    = { } val events = eventStreamReader.readStream( tenantId.id, aggregateId.id, revision.map(_ => VersionRange(to = revision))) events.lastOption.map { lastEvent => } val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) AggregateSnapshot(aggregate, lastEvent.aggregateVersion, events.head.timestamp, lastEvent.timestamp)
  40. val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) trait AggregateReducer[AggregateType] { }

    def reduce(aggregate: AggregateType, event: EventData): AggregateType def apply(events: Seq[EventData])(initialState: AggregateType) = { events.foldLeft(initialState)(reduce) }
  41. val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) trait AggregateReducer[AggregateType] { }

    def reduce(aggregate: AggregateType, event: EventData): AggregateType def apply(events: Seq[EventData])(initialState: AggregateType) = { events.foldLeft(initialState)(reduce) }
  42. val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) trait AggregateReducer[AggregateType] { }

    def reduce(aggregate: AggregateType, event: EventData): AggregateType def apply(events: Seq[EventData])(initialState: AggregateType) = { events.foldLeft(initialState)(reduce) }
  43. val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) trait AggregateReducer[AggregateType] { }

    def reduce(aggregate: AggregateType, event: EventData): AggregateType def apply(events: Seq[EventData])(initialState: AggregateType) = { events.foldLeft(initialState)(reduce) }
  44. val aggregate = reducer(events.map(e => e.data))(emptyState(aggregateId.id)) trait AggregateReducer[AggregateType] { }

    def reduce(aggregate: AggregateType, event: EventData): AggregateType def apply(events: Seq[EventData])(initialState: AggregateType) = { events.foldLeft(initialState)(reduce) }
  45. 1 Talk Started 2 Event Sourcing Presented 3 CQRS Demonstrated

    4 Trade-offs Explained Save it in your event store.