Share
Embed
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
Transcript
69 slides
-
@driesvints driesvints.com EVENT SOURCING IN LARAVEL EVENT SOURCING
IN LARAVEL WITH EVENTSAUCE WITH EVENTSAUCE -
@driesvints driesvints.com WHAT WILL WE BE USING? WHAT
WILL WE BE USING? eventsauce/eventsauce eventsauce/laravel-eventsauce (0.1.0) laravel/framework -
@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) -
@driesvints driesvints.com OUR DOMAIN & PROCESS OUR DOMAIN
& PROCESS -
@driesvints driesvints.com MODEL =/= ELOQUENT MODEL MODEL =/=
ELOQUENT MODEL COMMAND =/= CONSOLE COMMAND COMMAND =/= CONSOLE COMMAND -
@driesvints driesvints.com WHAT IS EVENT SOURCING? WHAT IS
EVENT SOURCING? -
@driesvints driesvints.com WELL, IT'S A LITTLE BIT LIKE
MAGIC... WELL, IT'S A LITTLE BIT LIKE MAGIC... -
@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. -
@driesvints driesvints.com eventsauce.io/docs/event-sourcing Ac ons performed on the
system result in events, which are communicated throughout the system. -
@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. -
@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. -
@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 -
@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(); -
@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 -
@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; -
@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); -
@driesvints driesvints.com READS BECOME EASIER READS BECOME EASIER
WRITES BECOME HARDER WRITES BECOME HARDER -
@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 -
@driesvints driesvints.com HISTORY NEVER CHANGES HISTORY NEVER CHANGES
NO MATTER WHAT NO MATTER WHAT -
@driesvints driesvints.com WHY WOULD YOU NEED EVENT SOURCING?
WHY WOULD YOU NEED EVENT SOURCING? -
@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... -
@driesvints driesvints.com WHEN SHOULD YOU USE EVENT SOURCING?
WHEN SHOULD YOU USE EVENT SOURCING? -
@driesvints driesvints.com PROCESS MODELLING PROCESS MODELLING Finite flows,
complex transi ons and transac ons. eventsauce.io/docs/event-sourcing -
@driesvints driesvints.com AUDIT TRAILS AUDIT TRAILS Cases where
audit trails are of paramount importance. eventsauce.io/docs/event-sourcing -
@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. -
@driesvints driesvints.com HOW DO YOU IMPLEMENT EVENT SOURCING?
HOW DO YOU IMPLEMENT EVENT SOURCING? -
@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 ); } -
@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() ); } -
@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... } } -
@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); } } -
@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 -
@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; // ... } -
@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() )); } } -
@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... } } -
@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... } } -
@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; } // ... } -
@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++; } // ... } -
@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(); } } -
@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); } } -
@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; } } -
@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'); }); -
@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! -
@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 -
@driesvints driesvints.com DISPATCHING MESSAGES DISPATCHING MESSAGES Messages are
dispatched by the MessageDispatcher Can be Asynchronous, Synchronous, or both! Messages are handled by Consumers -
@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 -
@driesvints driesvints.com PROJECTIONS PROJECTIONS Used to update read
models like: Database tables Report files/pdfs Your search indexes like Algolia, Elas cSearch, ... ... -
@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 -
@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... } -
@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 -
@driesvints driesvints.com PROCESS MANAGERS PROCESS MANAGERS Used to
react to events: Send email no fica ons Trigger builds Clear logs ... -
@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 -
@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); } } -
@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() ); } } -
@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() ); } -
@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) -
@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"... } } -
@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) -
@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 -
@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? -
@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 -
@driesvints driesvints.com READING MATERIAL READING MATERIAL eventsauce.io/docs/event-sourcing eventsauce.io/docs/architecture
eventsauce.io/docs/lifecycle michielrook.nl verraes.net -
@driesvints driesvints.com OTHER LIBRARIES OTHER LIBRARIES github.com/spa e/laravel-eventsauce
github.com/spa e/laravel-event-projector -
@driesvints driesvints.com THANKS! THANKS! driesvints.com @driesvints
fullstackbelgium.be fullstackeurope.com