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

Event Sourcing in Laravel with EventSauce

Event Sourcing in Laravel with EventSauce

In this talk we're going to take a look at what Event Sourcing is and how we can integrate it a Laravel application. We'll first briefly go through the theory behind Event Sourcing and after that we'll make use of the EventSauce library to show you a real practical approach at how to set up a basic app with some aggregate roots, some projections and some process managers. To wrap things up we'll go through some tips on how you can leverage Event Sourcing in specific situations.

You'll learn:
- Basic theory about event sourcing
- How to implement it in a Laravel app
- Using the eventsauce/laravel-eventsauce library
- A number of practical benefits you get from an event sourced application

Dries Vints

August 30, 2019
Tweet

More Decks by Dries Vints

Other Decks in Programming

Transcript

  1.  @driesvints  driesvints.com WHAT WILL WE BE USING? WHAT

    WILL WE BE USING? eventsauce/eventsauce eventsauce/laravel-eventsauce (0.1.0) laravel/framework
  2.  @driesvints  driesvints.com WHAT IS EVENTSAUCE? WHAT IS EVENTSAUCE?

    An easy way to introduce event sourcing into PHP projects Designed so storage and queueing mechanisms can be chosen based on your specific requirements It has test tooling, designed to work with an event sourcing mindset By Frank De Jonge (Author of FlySystem)
  3.  @driesvints  driesvints.com MODEL =/= ELOQUENT MODEL MODEL =/=

    ELOQUENT MODEL COMMAND =/= CONSOLE COMMAND COMMAND =/= CONSOLE COMMAND
  4.  @driesvints  driesvints.com WELL, IT'S A LITTLE BIT LIKE

    MAGIC... WELL, IT'S A LITTLE BIT LIKE MAGIC...
  5.  @driesvints  driesvints.com eventsauce.io/docs/event-sourcing Event sourcing is a way

    to model so ware where the emphasis is put on why (and how) things change, rather than focusing on the current state of the applica on.
  6.  @driesvints  driesvints.com eventsauce.io/docs/event-sourcing Ac ons performed on the

    system result in events, which are communicated throughout the system.
  7.  @driesvints  driesvints.com eventsauce.io/docs/event-sourcing Regular OOP-style modelling is closely

    modelled towards the data model. Our code reflects this data-centric view. When we test our code, we assert based on current applica on state.
  8.  @driesvints  driesvints.com eventsauce.io/docs/event-sourcing In event sourcing this concept

    is turned upside-down. The model is constructed using events that describe things that have happened in the past. These events are used to communicate change throughout the system.
  9.  @driesvints  driesvints.com COMMAND QUERY RESPONSIBILITY COMMAND QUERY RESPONSIBILITY

    SEGREGATION (CQRS) SEGREGATION (CQRS) Separate your read model from your write model Easily separate concerns and constraints Goes really well alongside Event Sourcing
  10.  @driesvints  driesvints.com WRITE MODEL WITHOUT EVENT SOURCING WRITE

    MODEL WITHOUT EVENT SOURCING CRUD like opera ons are performed on models which will overwrite any state we currently have in our persistent storage. $pullRequest = PullRequest::create($data); $pullRequest->update($data); $pullRequest->delete();
  11.  @driesvints  driesvints.com WRITE MODEL WITH EVENT SOURCING WRITE

    MODEL WITH EVENT SOURCING Commands are performed which will validate the data based on your business rules. Events are emi ed and saved in an event store. $pullRequest->open(new OpenPullRequest(...)); // emits PullRequestWasOpened $pullRequest->approve(new ApprovePullRequest(...)); // emits PullRequestWasApproved $pullRequest->merge(new MergePullRequest(...)); // emits PullRequestWasMerged
  12.  @driesvints  driesvints.com READ MODEL WITHOUT EVENT SOURCING READ

    MODEL WITHOUT EVENT SOURCING Data needs to be fetched from three individual tables: pull_requests, authors & comments $pullRequest = PullRequest::with('author', 'comments') ->find($id); $author = $pullRequest->author; $comments = $pullRequest->comments;
  13.  @driesvints  driesvints.com READ MODEL WITH EVENT SOURCING READ

    MODEL WITH EVENT SOURCING Instead of modelling for mul ple use-cases, you model your reads for their specific use-case. Data can be fetched from only one table: pull_requests $pullRequest = PullRequest::find($id); $author = $pullRequest['author']; $comments = json_decode($pullRequest['comments'], true);
  14.  @driesvints  driesvints.com EVENT SOURCING... EVENT SOURCING... ...MAKES THE

    EASY THING HARD ...MAKES THE EASY THING HARD ...BUT MAKES THE HARD THING EASY ...BUT MAKES THE HARD THING EASY
  15.  @driesvints  driesvints.com COOL STUFF YOU CAN DO COOL

    STUFF YOU CAN DO ⚡ Lightning Fast Reads ✨ Predict The Future ⏳ Time Travel Audi ng And much more...
  16.  @driesvints  driesvints.com PROCESS MODELLING PROCESS MODELLING Finite flows,

    complex transi ons and transac ons. eventsauce.io/docs/event-sourcing
  17.  @driesvints  driesvints.com AUDIT TRAILS AUDIT TRAILS Cases where

    audit trails are of paramount importance. eventsauce.io/docs/event-sourcing
  18.  @driesvints  driesvints.com WHEN SHOULD YOU NOT USE IT?

    WHEN SHOULD YOU NOT USE IT? When you have a CMS, CRM, or other CRUD-like systems, maybe you don't need event sourcing.
  19.  @driesvints  driesvints.com THE TRADITIONAL STATEFUL WAY THE TRADITIONAL

    STATEFUL WAY public function store(Request $request) { $user = $request->user(); $pullRequest = $user->pullRequests()->create( $request->only( 'title', 'description', 'origin', 'target' ) ); return redirect( '/laravel/framework/pull/'.$pullRequest->id ); }
  20.  @driesvints  driesvints.com THE EVENT SOURCING WAY THE EVENT

    SOURCING WAY public function store(Request $request) { $this->dispatchNow(new OpenPullRequest( $id = PullRequestId::generate(), // UUID $request->user()->id, $request->get('title'), $request->get('description'), $request->get('origin'), $request->get('target') )); return redirect( '/laravel/framework/pull/'.$id->toString() ); }
  21.  @driesvints  driesvints.com USE COMMANDS TO TRANSFER DATA USE

    COMMANDS TO TRANSFER DATA final class OpenPullRequest { // Properties... public function __construct( PullRequestId $pullRequestId, int $userId, string $title, string $description, string $origin, string $target ) { // Assign dependencies to properties... } }
  22.  @driesvints  driesvints.com RETRIEVE AGGREGATEROOT AND HANDLE COMMAND RETRIEVE

    AGGREGATEROOT AND HANDLE COMMAND use PullRequestAggregateRootRepository as Repository; final class OpenPullRequest { // Properties, constructor and getters... public function handle(Repository $repository): void { $pullRequest = $repository->retrieve( $this->pullRequestId ); $pullRequest->open($this); } }
  23.  @driesvints  driesvints.com ELOQUENT MODELS VS AGGREGATEROOTS ELOQUENT MODELS

    VS AGGREGATEROOTS Eloquent Models represent your data model AggregateRoots represent your processes and guard your business rules AggregateRoots can consist of mul ple Aggregates
  24.  @driesvints  driesvints.com THE AGGREGATEROOT THE AGGREGATEROOT namespace App\Domain\PullRequest;

    use EventSauce\EventSourcing\AggregateRoot; use EventSauce\EventSourcing\AggregateRootBehaviour; final class PullRequest implements AggregateRoot { use AggregateRootBehaviour; // ... }
  25.  @driesvints  driesvints.com HANDLING THE COMMAND HANDLING THE COMMAND

    final class PullRequest implements AggregateRoot { public function open(OpenPullRequest $command): void { // Validate business rules... $this->recordThat(new PullRequestWasOpened( $command->userId(), $command->title(), $command->description(), $command->origin(), $command->target() )); } }
  26.  @driesvints  driesvints.com VALIDATING BUSINESS RULES VALIDATING BUSINESS RULES

    final class PullRequest implements AggregateRoot { public function open(OpenPullRequest $command): void { if ($command->origin() === $command->target()) { throw OpenPullRequestFailed::branchesMatch(); } // Record event... } }
  27.  @driesvints  driesvints.com RECORDING THE UNHAPPY PATH RECORDING THE

    UNHAPPY PATH final class PullRequest implements AggregateRoot { public function merge(MergePullRequest $command): void { if ($this->approved === false) { $this->recordThat( new UnapprovedMergeWasAttempted() ); throw MergePullRequestFailed::unapproved(); } // Record event... } }
  28.  @driesvints  driesvints.com RECORDING THE EVENT RECORDING THE EVENT

    trait AggregateRootBehaviour { /** @var object[] */ private $recordedEvents = []; // ... protected function recordThat(object $event) { $this->apply($event); $this->recordedEvents[] = $event; } // ... }
  29.  @driesvints  driesvints.com APPLYING EVENTS APPLYING EVENTS trait AggregateRootBehaviour

    { // ... protected function apply(object $event) { $parts = explode('\\', get_class($event)); $this->{'apply' . end($parts)}($event); $this->aggregateRootVersion++; } // ... }
  30.  @driesvints  driesvints.com APPLYING THE EVENT APPLYING THE EVENT

    Is also run every me the aggregate is retrieved from its repository. final class PullRequest implements AggregateRoot { public function applyPullRequestWasOpened( PullRequestWasOpened $event ): void { $this->userId = $command->userId(); $this->origin = $command->origin(); $this->target = $command->target(); } }
  31.  @driesvints  driesvints.com PERSISTING EVENTS PERSISTING EVENTS Excep ons

    cascade through but events are s ll saved. public function handle(Repository $repository): void { $pullRequest = $repository->retrieve( $this->pullRequestId ); try { $pullRequest->open($this); } finally { $repository->persist($pullRequest); } }
  32.  @driesvints  driesvints.com REBUILDING THE AGGREGATE REBUILDING THE AGGREGATE

    trait AggregateRootBehaviour { public static function reconstituteFromEvents( AggregateRootId $aggregateRootId, Generator $events ): AggregateRoot { $aggregateRoot = new static($aggregateRootId); foreach ($events as $event) { $aggregateRoot->apply($event); } return $aggregateRoot; } }
  33.  @driesvints  driesvints.com THE EVENT STORE THE EVENT STORE

    Schema::create('domain_messages', function (Blueprint $table) { $table->bigIncrements('id'); $table->string('event_id', 36); $table->string('event_type', 100); $table->string('aggregate_root_id', 36)->nullable()->index() $table->dateTime('recorded_at', 6)->index(); $table->text('payload'); });
  34.  @driesvints  driesvints.com EVENT STORES EVENT STORES Append- &

    Read-Only Can live outside the main system Can live independently by AggregateRoot The only part that you cannot lose!
  35.  @driesvints  driesvints.com DOMAIN MESSAGES DOMAIN MESSAGES Events are

    transported in Domain Messages and are saved in an event store Messages contain the event (payload), aggregate root id, recorded mestamp Event payload is serialized before stored
  36.  @driesvints  driesvints.com DISPATCHING MESSAGES DISPATCHING MESSAGES Messages are

    dispatched by the MessageDispatcher Can be Asynchronous, Synchronous, or both! Messages are handled by Consumers
  37.  @driesvints  driesvints.com CONSUMERS CONSUMERS Two types of consumers:

    Projec ons: processes events to update "read models" Process Managers: listen to events and then perform ac ons
  38.  @driesvints  driesvints.com PROJECTIONS PROJECTIONS Used to update read

    models like: Database tables Report files/pdfs Your search indexes like Algolia, Elas cSearch, ... ...
  39.  @driesvints  driesvints.com WHY ARE PROJECTIONS USEFUL? WHY ARE

    PROJECTIONS USEFUL? Separate presenta onal state from the process you're modelling Read models are free from any constraints your domain model has Your read models can be very op mized this way Create as many read models as you like Super easy to rebuild read models
  40.  @driesvints  driesvints.com THE PROJECTION THE PROJECTION use App\Models\PullRequestPage;

    use EventSauce\EventSourcing\Message; use EventSauce\LaravelEventSourcing\Consumer; final class BuildPullRequestPage extends Consumer { public function handlePullRequestWasOpened( PullRequestWasOpened $event, Message $message ): void { PullRequestPage::create(...); } // Handle other events... }
  41.  @driesvints  driesvints.com REBUILDING PROJECTIONS REBUILDING PROJECTIONS Allows you

    to discard your current state of your read model and rebuild it from its event stream. Unfortunately not baked into EventSauce... ...but maybe in Laravel EventSauce
  42.  @driesvints  driesvints.com PROCESS MANAGERS PROCESS MANAGERS Used to

    react to events: Send email no fica ons Trigger builds Clear logs ...
  43.  @driesvints  driesvints.com WHY ARE PROCESS MANAGERS USEFUL? WHY

    ARE PROCESS MANAGERS USEFUL? Easily handle one- me ac ons Separate mul ple steps for your process Handle non-user interac ons Can be performed in the background and aren't blocking
  44.  @driesvints  driesvints.com THE PROCESS MANAGER THE PROCESS MANAGER

    use EventSauce\EventSourcing\Message; use EventSauce\LaravelEventSourcing\Consumer; final class PullRequestNotifier extends Consumer { public function handlePullRequestWasOpened( PullRequestWasOpened $event, Message $message ): void { Mail::to($maintainer) ->send(new PullRequestWasOpenedMail); } }
  45.  @driesvints  driesvints.com TESTING IN A BDD WAY TESTING

    IN A BDD WAY class MergingPullRequestTest extends PullRequestTestCase { /** @test */ public function it_can_be_merged() { $pullRequestId = $this->aggregateRootId(); $this->given( new PullRequestWasOpened($pullRequestId, ...), new PullRequestWasApproved($pullRequestId) )->when( new MergePullRequest($pullRequestId) )->then( new PullRequestWasMerged() ); } }
  46.  @driesvints  driesvints.com TESTING FAILURES TESTING FAILURES /** @test

    */ public function it_cannot_be_merged_when_not_approved() { $pullRequestId = $this->aggregateRootId(); $this->given( new PullRequestWasOpened($pullRequestId, ...) )->when( new MergePullRequest($pullRequestId) )->then( new UnapprovedMergeWasAttempted() )->expectToFail( new MergePullRequestFailed() ); }
  47.  @driesvints  driesvints.com OTHER THINGS EVENTSAUCE CAN DO OTHER

    THINGS EVENTSAUCE CAN DO Generate Commands and Events from yaml Message decora on to add metadata to your messages Event versioning (handling concurrency)
  48.  @driesvints  driesvints.com ONE QUERY TO RETRIEVE ALL EVENTS

    ONE QUERY TO RETRIEVE ALL EVENTS use EventSauce\EventSourcing\MessageRepository; class LaravelMessageRepository implements MessageRepository { public function retrieveAll(AggregateRootId $id): Generator { $payloads = $this->connection->select(' SELECT payload FROM domain_messages WHERE aggregate_root_id = ? ORDER BY recorded_at ASC ', [$id->toString()]); // Unserialize here and return by "yield"... } }
  49.  @driesvints  driesvints.com PERFORMANCE PERFORMANCE Objects usually don't live

    long enough for it to be a problem One query to retrieve all events from an aggregate In-memory event replaying is quite fast actually Use snapsho ng when you really need it (millions of events on a single aggregate)
  50.  @driesvints  driesvints.com REVISIT YOUR APP IN THE PAST

    REVISIT YOUR APP IN THE PAST Replay events un l a certain me Only projec ons are replayed Combine with your VCS to also show the app code from that me Neat for debugging, demo'ing, etc
  51.  @driesvints  driesvints.com HOW DO I START USING EVENT

    SOURCING HOW DO I START USING EVENT SOURCING IN AN EXISTING SYSTEM? IN AN EXISTING SYSTEM?
  52.  @driesvints  driesvints.com USING EVENT SOURCING IN AN EXISTING

    SYSTEM USING EVENT SOURCING IN AN EXISTING SYSTEM Start with a simple process Make sure you have your process modelled first (use Event Storming) Introduce a "start event" with all of your current data Don't try to be smart and "guess" the past events because you'll fail miserable