Capture all changes to an application state as a sequence of events.
Martin Fowler
Instead of storing just the current state of the data in a domain, use an append-only store to record the full series of actions taken on that data.
https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing
class FootballGame {
private UUID $id;
private bool $hasStarted = false;
private bool $hasEnded = false;
private int $teamAScore = 0;
private int $teamBScore = 0;
public function __contstructor() {
$this->id = UUID::uuid4();
}
...
}
class FootballGame {
...
public function startGame(): void {
if ($this->hasStarted) {
throw new GameAlreadyStarted();
}
$this->hasStarted = true;
}
public function endGame(): void {....}
...
}
class FootballGame {
...
public function score(Target $target): void {
if (!$this->hasStarted || $this->hasEnded) {
throw new GameIsInactive();
}
if ($target->isTeamA()) {
$this->teamBScore++;
} else {
$this->teamAScore++;
}
}
}
class FootballGame {
private UUID $id;
private DateTime $startDate;
private DateTime $endDate;
/** @var Goal */
private array $goals = [];
public function __contstructor(
private Team $teamA,
private Team $teamB
) {
$this->id = UUID::uuid4();
}
...
}
class FootballGame {
...
public function startGame(): void {
if (isset ($this->startDate)) {
throw new GameAlreadyStarted();
}
$this->startDate = new DateTime();
}
...
public function score(Target $target, Player $scorer): void {
if (!$this->hasStarted() || $this->hasEnded()) {
throw new GameIsInactive();
}
$this->goals[] = new Goal($target, $scorer, new DateTime());
}
}
class FootballGame {
...
/** @return Goal[] */
public function getGoalsForTeam(Team $team): array {
return array_filter(
$this->goals,
fn (Goal $g) => $g->target()->isTeam($team)
);
}
public function getGoalsCountForTeam(Team $team): int {
return count($this->getGoalsForTeam($tema));
}
...
}
class FootballGame {
private UUID $id;
private DateTime $firstHalfStartTime;
private DateTime $firstHalfEndTime;
private DateTime $secondHalfStartTime;
private DateTime $firstExtentionStartTime;
...
/** @var Goal[] */
private array $goals = [];
/** @var Touch[] */
private array $touches = [];
/** @var Corner[] */
private array $corners = [];
/** @var Out[] */
private array $outs = [];
/** @var PenaltyShot[] */
private array $penaltyShots = [];
/** @var PenalyCard[] */
private array $penaltyCards = [];
/** @var PlayerSwap[] */
private array $playerSwaps = [];
...
}
class FootballGame {
private array $events = [];
public function __construct(Team $teamA, Team $teamB) {
$this->events[] = GameCreatedEvent(UUID::uuid4(), $teamA, $teamB);
}
public function startFirstHalf(): void {
if ($this->hasFirstHalfStarted()) {
throw new FirstHalfHasAlreadyStartedException();
}
$this->events[] = new FirstHalfStartedEvent(new DateTime());
}
...
}
class FootballGame {
...
public function score(Target $target, Player $scorer): void {
if ($this->isStopped()) {
throw new GameIsStoppedException();
}
$this->events[] = new GoalEvent($target, $player, new DateTime());
}
...
}
class FootballGame {
...
public function swapPlayers(Player $out, Player $in): void {
if (!$this->isStopped()) {
throw new GameIsNotStoppedException();
}
if ($this->playerSwapsCount($out->team()) === 3) {
throw new TeamCanNotSwapAnyMorePlayersException();
}
$this->events[] = new PlayersSwappedEvent($out->id, $in->id);
}
...
}
class FootballGame {
...
private function hasFirstHalfStarted(): bool {
foreach ($this->events as $e) {
if ($e instanceof FirstHalfStartedEvent) {
return true;
}
}
return false;
}
...
}
class FootballGame {
...
private function isStopped(): bool {
$isStopped = true;
foreach ($this->events as $e) {
if ($e instanceof GameStoppedEvent) {
$isStopped = true;
} else if (
$e instanceof GameResumedEvent
|| $e instanceof FirstHalfStartedEvent
) {
$isStopped = false;
}
}
return $isStopped;
}
...
}
class FootballGame {
...
private function playerSwapsCount(Team $t): int {
$count = 0;
foreach ($this->events as $e) {
if ($e instanceof PlayersSwappedEvent && $t->hasPlayer($e->out->id)) {
$count++;
}
}
return $count;
}
...
}
The order of the events is of UTTER importance!!!
final class PlayersSwappedEvent {
public function __construct(
public readonly UUID $out,
public readonly UUID $in
) {}
}
We definitely need:
It's good to have:
final class EventEnvelope {
private function __construct(
public readonly UUID $id,
public readonly string $type,
public readonly UUID $aggregateId,
public readonly int $aggregateVersion,
public readonly object $payload
) {}
protected static function createNew(
object $payload,
UUID $aggregateId,
int $aggregateVersion
): static {...}
public static function fromArray(array $data): static {...}
public function toArray(): array {...}
}
Essentially a database which:
class FootballGame {
private array $events = [];
private function __construct() {};
public static function createNew(Team $teamA, Team $teamB): self {
$instance = new self();
$instance->events[] = GameCreatedEvent(UUID::uuid4(), $teamA, $teamB);
return $instance;
}
public static function fromEvents(array $events): self {
$instance = new self();
$instance->events = $events;
return $instance;
}
..
}
class GameStatistics {
public int $totalShots = 0;
public int $onTargetShots = 0;
public int $goals = 0;
...
public static function fromEvents(array $events): self {
$instance = new self();
foreach ($events as $e) {
if ($e instanceof Shot) {
$this->totalShots++;
if ($e->onTarget) {
$this->onTargetShots++;
}
if ($e->isGoal) {
$this->goals++;
}
}
...
}
}
}
class TeamStatistics {
public int $goals = 0;
public float $possesionPercent = 0;
public float $passPercent = 0;
public float $aerialsWon = 0;
public float $rating = 0;
...
public static function fromEvents(array $events): self {
$instance = new self();
foreach ($events as $e) {
...
}
}
}
class FootballGame {
private UUID $id;
private bool $hasStarted = true;
private bool $hasEnded = false;
private int $teamAScore = 2;
private int $teamBScore = 3;
...
}
class FootballGame {
...
public function score(Target $target): void {
if (!$this->hasStarted || $this->hasEnded) {
throw new GameIsInactive();
}
$this->recordEvent(new GoalEvent($target));
}
private function applyGoalEvent(GoalEvent $e) {
if ($e->target->isTeamA()) {
$this->teamBScore++;
} else {
$this->teamAScore++;
}
}
}
class FootballGame {
...
private function recordEvent(Event $e) {
$this->events[] = $e;
$this->applyEvent($e);
}
private function applyEvent(Event $e) {
match ($e::class) {
GameStartedEvent::class => $this->applyGameStartedEvent($e),
GoalEvent::class => $this->applyGoalEvent($e),
...
}
}
}
class FootballGame {
...
public static function fromEvents(array $events): self {
$instance = new self();
foreach ($events as $e) {
$instance->applyEvent($e);
}
}
}
CREATE TABLE games (
id UUID not null,
has_started boolean not null,
has_ended boolean not null,
team_a_score unsigned int not null,
team_b_score unsigned int not null,
aggregate_version unsigned int not null
)
Different projections can use different databases for storage!!!
Events are dispatched...
Subscribers/Consumers can be:
We can put the events on a network and have...
Milko Kosturkov
@mkosturkov
linkedin.com/in/milko-kosturkov
mailto: mkosturkov@gmail.com
These slides:
https://slides.com/milkokosturkov/event-sourcing-the-very-basics