developer

I LOVE Legacy!

Fork Knife

Learning Objectives

  • Basics of Event Sourcing using an example of a library (the kind where you check out books)
  •  

  • Three projects I have worked on and the ways we used Event Sourcing on them

Before we begin

Learning Event-Sourcing (or DDD) is tough! i

Be kind to learners (including yourself)

Event-Sourcing isn't for your whole application

Event Sourcing

The fundamental idea of Event Sourcing is that of ensuring every change to the state of an application is captured in an event object, and that these event objects are themselves stored in the sequence they were applied for the same lifetime as the application state itself.

Martin Fowler

Events and Listeners

An Event represented as a diamond and a Listener represented as an ear

An Event

What happened?
BookWasCheckedOut

What do I need to remember about it?
(book, patron, date)

Event Attributes

  • Save only what you need to preserve
  • The rest can be looked up

(book id, patron id, date)

Event Class


<?php

namespace Library\Events;

use Library\Support\Event;

class BookWasCheckedOut
{

    /**
     * @var DateTime
     */
    protected $checkoutDate;

    /**
     * @var int
     */
    protected $patronId;

    /**
     * @var int
     */
    protected $bookId;

    public function __construct(DateTime $checkoutDate, PatronId $patronId, BookId $bookId)
    {

        $this->checkoutDate = $checkoutDate;
        $this->patronId = $patronId;
        $this->bookId = $bookId;
    }

    /**
    * @return array
    */
    public function serialize()
    {
        return [
            'checkout_date' => $this->$checkoutDate->toString(),
            'patron_id' => $this->patronId->toNative(),
            'book_id' => $this->bookId->toNative(),
        ];
    }

    /**
    * @param array $data
    *
    * @return static
    */
    public static function deserialize($data)
    {
        return new BookWasCheckedOut(
            DateTime::createFromFormat('j-M-Y', $data['checkout_date']),
            PatronId::fromNative($data['patron_id']),
            BookId::fromNative($data['book_id'])
        );
    }

     /**
     * @return int
     */
    public function getBookId()
    {
        return $this->bookId;
    }

    /**
     * @return DateTime
     */
    public function getCheckoutDate(): DateTime
    {
        return $this->checkoutDate;
    }

    /**
     * @return int
     */
    public function getPatronId(): int
    {
        return $this->patronId;
    }


}

Rules for Events

  • Usually named as past-tense verbs
  • RARELY changed
  • Never deleted
  • Has attributes that are values
    • not model, object, collection, or aggregate root
3 symbols representing events of a deposit, a correction, and a deposit

Don't store objects

objects can change

If We Stored Objects in an Event...

Event With Object
Jeff Goldblum in Jurassic Park saying you never stopped to think whether you should

Events rarely change

  • The part of the code that will change is most likely the result that follows that event.
  • The structure of the resulting data is more likely to change than the thing that happened

One event with one listener handling the event

One event with two listeners handling the event differently, one is thrown away

One event with two listeners handling the event differently

Reasons to use Events

  • State transitions are important
  • We need an audit log, proof of the state we are currently in
  • The history of what happened is more important than the current state
  • Events are replayable if behavior in your application changes

Domain Message

Event Store

  • Domain-specific database
  • Based on a Publish-Subscribe message pattern

EventStore

Projector

<?php

namespace Library\ReadModel;

use Library\Events\BookWasCheckedIn;
use Library\Events\BookWasCheckedOut;
use Library\Events\BookAddedToBookshelf;
use App\Support\ReadModel\Replayable;
use App\Support\ReadModel\SimpleProjector;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Connection;

class BookshelfProjector extends SimpleProjector implements Replayable
{

    /**
     * @var Connection
     */
    private $connection;

    /**
     * @var string table we're playing events into
     */
    private $table = 'proj_bookshelf';

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    public function beforeReplay()
    {
        $builder = $this->connection->getSchemaBuilder();

        $builder->dropIfExists('proj_bookshelf');
        $builder->create('proj_bookshelf_tmp', function (Blueprint $schema) {
            $schema->string('book_id');
            $schema->string('book_title');
            $schema->string('book_author');
            $schema->string('status');
            $schema->string('checkout_date');
            $schema->string('patron_id');

            $schema->primary('book_id');
        });

        $this->table = 'proj_bookshelf_tmp';
    }

    public function afterReplay()
    {
        $builder = $this->connection->getSchemaBuilder();

        $builder->dropIfExists('proj_bookshelf');
        $builder->rename('proj_bookshelf_tmp', 'proj_bookshelf');

        $this->table = 'proj_bookshelf';
    }

    /**
    * @param BookWasCheckedOut $event
    */
    public function applyBookWasCheckedOut(BookWasCheckedOut $event)
    {
        $bookshelfItem = BookshelfItem::where('id', $event->bookId);

        $book->status = 'Checked Out';
        $book->checkout_date = $event->checkoutDate;
        $book->patron_id = $event->patronId;

        $book->save();
    }

    /**
    * @param BookAddedToBookshelf $event
    */
    public function applyBookAddedToBookshelf(BookAddedToBookshelf $event)
    {
        $bookshelfItem = new BookshelfItem();
        $bookshelfItem->setTable($this->table);

        $bookshelfItem->bookId = $event->bookId;
        $bookshelfItem->bookTitle = $event->bookTitle;
        $bookshelfItem->bookAuthor = $event->bookAuthor;
        $bookshelfItem->status = 'on shelf';

        $bookshelfItem->save();
    }


    /**
    * @param BookWasReturned $event
    */
    public function applyBookWasReturned(BookWasReturned $event)
    {
        $bookshelfItem = BookshelfItem::where('id', $event->bookId);

        $book->status = 'Available';
        $book->checkout_date = '';
        $book->patron_id = '';

        $book->save();
    }
}

A set of event handlers that work together to build and maintain a table to be accessed by the read model.

Read Model

Read Model table with highlighted rows and queries

Read Model

<?php

namespace Library\ReadModel;


use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;

/**
* @codeCoverageIgnore
*/
class Bookshelf extends Model
{
    protected  $table = 'proj_bookshelf';
    public $incrementing = false;
    public $timestamps = false;

    public static function lookupLoansFor($patronId)
    {
        return static::where('patron_id', $patronId)->get();
    }

    public function lookupAvailableBooks()
    {
        return static::where('status', 'Available')->get();
    }

    public function lookupOverdueBooks()
    {
        return static::where('checkout_date', '<', date('Y-m-d', strtotime('-7 days')))->get();
    }
}

Creating the events

  • An event is created only after validation
    • Directly in a controller 'checkout' method
    • Using a Check Out Book Command and Handler

CQRS


Command and Query Response Segregation

    An application architecture pattern commonly used with event sourcing

    CQRS involves splitting an application into two parts internally.

CQRS

  • Command is any method that mutates state
  • Query is any method that returns a value
  • Should only be used on specific portions of a system, not the system as a whole

Command Handler

  1. Validate the command on its own merits.
  2. Validate the command on the current state of the aggregate.
  3. If validation is successful, create an event(s)
  4. Attempt to persist the new events. If there's a concurrency conflict during this step, retry or exit.

Command Handler example

public function update(Request $request)
    {
        // $request has book id, patron id

        try {

            $command = new CheckOutBook($request->bookId, $request->patronId);

            $this->bookLendingService->handleCheckOutBook($command);

        } catch (InvalidUserException $e) {
            return response()->json("Not authorized to check out a book.", Response::HTTP_FORBIDDEN);
        } catch (BookUnavailableException $e) {
           return response()->json("Book was not available to be checked out", 400);

    }

    <------ dynamic command handler  ------>

    private function handle(Command $command)
    {
        $method = $this->getHandleMethod($command);

        if (! method_exists($this, $method)) {
            return;
        }

        $this->$method($command);
    }

    private function getHandleMethod(Command $command)
    {
        return 'handle' . class_basename($command);
    }

    <-------- handle check out book command --------->

    public function handleCheckoutOutBook(CheckOutBook $command)
    {
        $book = Book::findOrFail($command->bookId);
        $patron = Patron::findOrFail($command->patronId);

        if (!$book->isAvailable()) {
            throw new BookUnavailableException();
        }

        if (!$patron->isAuthorized()) {
            throw new InvalidUserException();
        }

        //record the event
        $this->record(
            new BookWasCheckedOut(date("Y-m-d H:i:s"),
                $patron->getId(),
                $book->getId())
        );


    }
    

Optimize

  • Separate the load from reads and writes allowing you to scale each independently.
    • All commands go into a WriteService
    • All queries go into a ReadService

You can choose which is best

  • CRUD
  • Event-Sourcing
  • Event-Sourcing with CQRS
  • Event-Sourcing, CQRS, DDD

My Projects

  • Scholarships
  • Course Registration
  • Threat Reports
Scholarships Project
bounded contexts
bounded contexts with Events

Flexible to Changes

  • Selection Committee would never take away an award
    • until they did
  • New academic year, new event store

Scholarships wrap-up

  • Modernized the code in pieces
  • View events from multiple contexts
  • Flexible to changes in the application

Student Enrollment Process

  • For Distance Education Students in the College of Engineering
  • Rewrote Application
  • ES to follow the process
  • Status drop-down versus events

Began with Paper Forms

A lot of our systems were built to replace paper processes

They often closely map to this physical form.

FormProcess

Paper Forms handling state

stamped application

Status labels are like a rubber stamp

Status doesn't always communicate why or what happened

Compensating Measures: diagram of a table with a status column added and a drawing of a table with a related status table

How workflows become complex

a diagram of a request coming in to be reviewed and the admin sending it either to approved, rejected, or hold piles; but hold has another fork for reasons and hold can be for academic or financial reasons

Paper Forms handling state

Paper forms sorted for a process

Piles indicate status of the form

A simple status drop-down

A "simple" status drop-down

Something happened

Status is a reflection of something that happened

There is ONE of each status + reasons/details

Events can record what happened

stamped application

Student Enrollment Wrap-up

  • Streamlined Process
  • Connected the system to Student Information System
  • Facilitated communication between students and admins
inquest logo

diagram

Analysis

Data Filtered for User Access

Inquest wrap-up

  • Events created by the Engine; used in Customer-facing Site
  • Events capture Session Threat History
  • Optimize the results for Read, reducing time to retrieve large amounts of data
  • Results are still filtered by a user's access
  • Full audit log
  • These events will lead to more improvements

Planned Improvements

  • Fewer complex DB queries
  • Could separate us from older schema; or reduce dependence on it
  • Separate the logic of what an event means based on context and purpose
  • Flexible to change, our interpretation of events can change, and we can rebuild projections without losing the full history

Thank you!

explore ddd