Slide 1

Slide 1 text

Event sourcing in production Willem-Jan Zijderveld @willemjanz [email protected] joind.in/16227

Slide 2

Slide 2 text

Event sourcing

Slide 3

Slide 3 text

Short introduction into our domain

Slide 4

Slide 4 text

Job Marketing Platform

Slide 5

Slide 5 text

Choosing the best channels to find your candidates

Slide 6

Slide 6 text

Online Job Marketing

Slide 7

Slide 7 text

1. Create a campaign for your target group

Slide 8

Slide 8 text

2. You get a recommendation

Slide 9

Slide 9 text

3. You can customize your campaign

Slide 10

Slide 10 text

Why event sourcing?

Slide 11

Slide 11 text

You are throwing away data!

Slide 12

Slide 12 text

By only saving last known state

Slide 13

Slide 13 text

You don't know the previous state

Slide 14

Slide 14 text

You don't know why something changed

Slide 15

Slide 15 text

How does event sourcing help?

Slide 16

Slide 16 text

Using events to store changes

Slide 17

Slide 17 text

Describe with your events what happened

Slide 18

Slide 18 text

Describe with your events why something happened

Slide 19

Slide 19 text

Use the events to calculate the current state

Slide 20

Slide 20 text

Allows you to go back in time

Slide 21

Slide 21 text

Helps to understand the flow in your application

Slide 22

Slide 22 text

Example: Discarding the customizations

Slide 23

Slide 23 text

CRUD

Slide 24

Slide 24 text

Keep track if channel was part of recommendation

Slide 25

Slide 25 text

Keep track if channel was part of recommendation Remove the non-recommended channels

Slide 26

Slide 26 text

Keep track if channel was part of recommendation How about removed channels?

Slide 27

Slide 27 text

Keep track if channel was part of recommendation Implement soft-delete

Slide 28

Slide 28 text

Keep track if channel was part of recommendation Implement soft-delete for new campaigns

Slide 29

Slide 29 text

Event sourcing

Slide 30

Slide 30 text

Just rewind

Slide 31

Slide 31 text

Just rewind Campaign Created For Target Group Channel Removed From Campaign Channel Added To Campaign Customization Discarded

Slide 32

Slide 32 text

Just rewind Campaign Created For Target Group Channel Removed From Campaign Channel Added To Campaign Customization Discarded

Slide 33

Slide 33 text

Just rewind Campaign Created For Target Group Channel Removed From Campaign Channel Added To Campaign Customization Discarded

Slide 34

Slide 34 text

Just rewind Campaign Created For Target Group Channel Removed From Campaign Channel Added To Campaign Customization Discarded

Slide 35

Slide 35 text

How?

Slide 36

Slide 36 text

With the help of CQRS and Domain Driven Design

Slide 37

Slide 37 text

And the help of Broadway github.com/qandidate-labs/broadway

Slide 38

Slide 38 text

Creating a new campaign

Slide 39

Slide 39 text

Campaign Event Store Create Campaign For Target Group Campaign Created For Target Group

Slide 40

Slide 40 text

Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto $dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }

Slide 41

Slide 41 text

Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto $dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }

Slide 42

Slide 42 text

Dispatching the command /** @Route("/campaigns", methods={"POST"}) */ public function createCampaignAction(CreateCampaignDto $dto) { $campaignId = Webshop\CampaignId::generate(); $this->commandBus->dispatch(new CreateCampaignForTargetGroup( $campaignId, $this->companyId, $this->accountId, TargetGroup::describe( TargetGroup\JobTitle::fromString($dto->title), TargetGroup\Region::fromId($dto->region), TargetGroup\JobCategory::fromId($dto->jobCategory), TargetGroup\EducationLevel::fromId($dto->educationLevel), TargetGroup\JobLevel::fromId($dto->jobLevel), TargetGroup\Industry::fromId($dto->industry) ) )); return new JsonResponse( ['id' => (string) $campaignId], JsonResponse::HTTP_CREATED ); }

Slide 43

Slide 43 text

Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command ) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }

Slide 44

Slide 44 text

Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command ) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }

Slide 45

Slide 45 text

Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command ) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender ); $this->campaignRepository->save($campaign); }

Slide 46

Slide 46 text

Action → Reaction // Campaign.php public static function createForTargetGroup(/* args */) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }

Slide 47

Slide 47 text

Action → Reaction // Campaign.php public static function createForTargetGroup(/* args */) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }

Slide 48

Slide 48 text

Action → Reaction // Campaign.php public static function createForTargetGroup(/* args */) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }

Slide 49

Slide 49 text

Action → Reaction // Campaign.php public static function createForTargetGroup(/* args */) { $campaign = new Campaign(); $campaignChannels = $recommender->recommend(/* args */); $campaign->apply(new CampaignCreatedForTargetGroup( $campaignId, $companyId, $accountId, Channels::fromCollection($campaignChannels), $targetGroup, $campaign->calculateCampaignPrice($campaignChannels), $campaign->calculateEstimatedOutcome($campaignChannels) )); return $campaign; }

Slide 50

Slide 50 text

// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[] = DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }

Slide 51

Slide 51 text

// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[] = DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }

Slide 52

Slide 52 text

Campaign Event Store Create Campaign For Target Group Campaign Created For Target Group

Slide 53

Slide 53 text

// Campaign.php protected function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $this->id = $event->getCampaignId(); $this->channels = $event->getRecommendedChannels()->toArray(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->targetGroup = $event->getTargetGroup(); $this->setInitialRecommendation( $this->channels, $this->estimatedOutcome, $this->campaignPrice ); }

Slide 54

Slide 54 text

// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[] = DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }

Slide 55

Slide 55 text

// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[] = DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }

Slide 56

Slide 56 text

// Broadway: EventSourcedAggregateRoot.php public function apply($event) { $this->handleRecursively($event); $this->playhead++; $this->uncommittedEvents[] = DomainMessage::recordNow( $this->getAggregateRootId(), $this->playhead, new Metadata(), $event ); }

Slide 57

Slide 57 text

Handling the command // CampaignCommandHandler.php public function handleCreateCampaignForTargetGroup( CreateCampaignForTargetGroup $command ) { $campaign = Campaign::createForTargetGroup( $command->getCampaignId(), $command->getCompanyId(), $command->getRecruiterId(), $command->getTargetGroup(), $this->recommender, $this->portfolio, $this->aidaMetricsCalculator ); $this->campaignRepository->save($campaign); }

Slide 58

Slide 58 text

Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate) { // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }

Slide 59

Slide 59 text

Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate) { // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }

Slide 60

Slide 60 text

Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate) { // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }

Slide 61

Slide 61 text

It happened in the past

Slide 62

Slide 62 text

Append only

Slide 63

Slide 63 text

Campaign Event Store Create Campaign For Target Group Campaign Created For Target Group

Slide 64

Slide 64 text

How would we load a campaign?

Slide 65

Slide 65 text

Campaign Event Store Campaign Created For Target Group

Slide 66

Slide 66 text

// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream = $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }

Slide 67

Slide 67 text

// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream = $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }

Slide 68

Slide 68 text

// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream = $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }

Slide 69

Slide 69 text

// Broadway: EventSourcingRepository.php public function load($id) { try { $domainEventStream = $this->eventStore->load($id); return $this->aggregateFactory->create( $this->aggregateClass, $domainEventStream ); } catch (EventStreamNotFoundException $e) { throw AggregateNotFoundException::create($id, $e); } } // Broadway: EventSourcedAggregateRoot.php public function initializeState(DomainEventStreamInterface $stream) { foreach ($stream as $message) { $this->playhead++; $this->handleRecursively($message->getPayload()); } }

Slide 70

Slide 70 text

Let's add a channel

Slide 71

Slide 71 text

Campaign Event Store Add Channel To Campaign Channel Added To Campaign

Slide 72

Slide 72 text

// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel = $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }

Slide 73

Slide 73 text

// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel = $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }

Slide 74

Slide 74 text

// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel = $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }

Slide 75

Slide 75 text

// CampaignCommandHandler.php public function handleAddChannelToCampaignCommand( AddChannelToCampaignCommand $command ) { $channel = $this->portfolio->getById($command->getChannelId()); $campaign = $this->campaignRepository->load( $command->getCampaignId() ); $campaign->addChannel($channel, $this->aidaMetricsCalculator); $this->campaignRepository->save($campaign); }

Slide 76

Slide 76 text

// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator ) { $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }

Slide 77

Slide 77 text

// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator ) { $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }

Slide 78

Slide 78 text

// Campaign.php public function addChannel( ChannelInPortfolio $channel, AidaMetricsCalculator $aidaMetricsCalculator ) { $this->guardCannotModifyOrderedCampaign(); $channelInCampaign = new ChannelInCampaign(/* $args */); $channels = array_merge($this->channels, [$channelInCampaign]); $campaignPrice = $this->calculateCampaignPrice($channels); $estimatedOutcome = $this->calculateEstimatedOutcome($channels); $this->apply(new ChannelAddedToCampaign( $this->id, $channelInCampaign, $campaignPrice, $estimatedOutcome )); }

Slide 79

Slide 79 text

// Campaign.php protected function applyChannelAddedToCampaign( ChannelAddedToCampaign $event ) { $this->channels[] = $event->getChannel(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event- >getEstimatedOutcomeOfCampaign(); $this->isCustomized = true; }

Slide 80

Slide 80 text

How do we get a listing of the campaigns?

Slide 81

Slide 81 text

We'll create a read model

Slide 82

Slide 82 text

Campaign Event Store Campaign Created For Target Group

Slide 83

Slide 83 text

Campaign Event Store Campaign Created For Target Group Read Model Store

Slide 84

Slide 84 text

Campaign Event Store Campaign Created For Target Group Read Model Store MySQL Read Model Store Redis

Slide 85

Slide 85 text

Saving the events // Broadway: EventSourcingRepository.php public function save(AggregateRoot $aggregate) { // maybe we can get generics one day.... ;) Assert::isInstanceOf($aggregate, $this->aggregateClass); $eventStream = $aggregate->getUncommittedEvents(); $this->eventStore->append( $aggregate->getAggregateRootId(), $eventStream ); $this->eventBus->publish($eventStream); }

Slide 86

Slide 86 text

Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event, DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }

Slide 87

Slide 87 text

Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event, DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }

Slide 88

Slide 88 text

Creating read models // CampaignOverviewProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event, DomainMessage $message ) { $this->repository->save(new CampaignOverview( $event->getCampaignId(), $event->getCompanyId(), $event->getTargetGroup()->getJobTitle(), $message->getRecordedOn() )); }

Slide 89

Slide 89 text

Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }

Slide 90

Slide 90 text

Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }

Slide 91

Slide 91 text

Creating read models // CampaignDetailsProjector.php public function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $campaignDetails = new CampaignDetails( $event->getCampaignId(), $event->getRecruiterId(), $event->getTargetGroup(), $event->getCompanyId(), $event->getRecommendedChannels()->toArray() ); $campaignDetails->updatePrice($event->getCampaignPrice()); $campaignDetails->updateEstimatedOutcome( $event->getEstimatedOutcome() ); $this->repository->save($campaignDetails); }

Slide 92

Slide 92 text

Using the history to your advantage

Slide 93

Slide 93 text

Discard Customizations

Slide 94

Slide 94 text

// Campaign.php protected function applyCampaignCreatedForTargetGroup( CampaignCreatedForTargetGroup $event ) { $this->id = $event->getCampaignId(); $this->channels = $event->getRecommendedChannels()->toArray(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->targetGroup = $event->getTargetGroup(); $this->setInitialRecommendation( $this->channels, $this->estimatedOutcome, $this->campaignPrice ); }

Slide 95

Slide 95 text

// Campaign.php public function discardCustomization() { $this->guardCannotModifyOrderedCampaign(); if ($this->isCustomized === false) { return; } $this->apply(new CustomizationDiscarded( $this->id, $this->initialRecommendation['channels'], $this->initialRecommendation['campaignPrice'], $this->initialRecommendation['estimatedOutcome'] )); }

Slide 96

Slide 96 text

// Campaign.php protected function applyCustomizationDiscarded( CustomizationDiscarded $event ) { $this->channels = $event->getRecommendedChannels(); $this->campaignPrice = $event->getCampaignPrice(); $this->estimatedOutcome = $event->getEstimatedOutcome(); $this->isCustomized = false; }

Slide 97

Slide 97 text

Where are the tests?

Slide 98

Slide 98 text

Scenario testing

Slide 99

Slide 99 text

Given When Then

Slide 100

Slide 100 text

Campaign Created For Target Group Add Channel To Campaign Channel Added To Campaign

Slide 101

Slide 101 text

$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);

Slide 102

Slide 102 text

$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);

Slide 103

Slide 103 text

$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);

Slide 104

Slide 104 text

$channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(new AddChannelToCampaignCommand( $this->campaignId, $channelId )) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);

Slide 105

Slide 105 text

Alternative scenario test $channelId = new ChannelId('42'); $this->scenario ->given([ new CampaignCreatedForTargetGroup( $this->campaignId, // $args ) ]) ->when(function (Campaign $campaign) { $campaign->addChannel($channelId) }) ->then([ new ChannelAddedToCampaign( $this->campaignId, $this->stubbedChannel($channelId), new CampaignPrice(Money::EUR(40000), Money::EUR(0)), new EstimatedOutcome(/* snip */) ) ]);

Slide 106

Slide 106 text

You can do something similar to test your read models

Slide 107

Slide 107 text

Using the history to correct mistakes

Slide 108

Slide 108 text

Replaying events to fix read models

Slide 109

Slide 109 text

Replaying events to create new read models

Slide 110

Slide 110 text

Very powerful

Slide 111

Slide 111 text

Very tricky

Slide 112

Slide 112 text

Replaying events Event Store Read Model Store e v e n t b u s Read Model Store

Slide 113

Slide 113 text

Replaying events Event Store Read Model Store e v e n t b u s Email Notification Processor

Slide 114

Slide 114 text

Find a solution for your specific problem

Slide 115

Slide 115 text

Questions? joind.in/16227 freenode: #qandidate github.com/qandidate-labs/broadway

Slide 116

Slide 116 text

Symfony User Group The Renaissance 10 december http://www.meetup.com/Symfony-User-Group-NL