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

Event Sourcing in practice @ Qandidate.com

Event Sourcing in practice @ Qandidate.com

A talk I gave first at Domcode Conference 2015.

Willem-Jan Zijderveld

November 14, 2015
Tweet

More Decks by Willem-Jan Zijderveld

Other Decks in Programming

Transcript

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

    View full-size slide

  2. Event sourcing

    View full-size slide

  3. Short introduction
    into our domain

    View full-size slide

  4. Job Marketing Platform

    View full-size slide

  5. Choosing the
    best channels
    to find your candidates

    View full-size slide

  6. Online Job Marketing

    View full-size slide

  7. 1. Create a campaign for your target group

    View full-size slide

  8. 2. You get a recommendation

    View full-size slide

  9. 3. You can customize your campaign

    View full-size slide

  10. Why event sourcing?

    View full-size slide

  11. You are
    throwing away data!

    View full-size slide

  12. By only saving
    last known state

    View full-size slide

  13. You don't know the
    previous state

    View full-size slide

  14. You don't know why
    something changed

    View full-size slide

  15. How does event sourcing
    help?

    View full-size slide

  16. Using events to store
    changes

    View full-size slide

  17. Describe with your events
    what happened

    View full-size slide

  18. Describe with your events
    why something happened

    View full-size slide

  19. Use the events to
    calculate the current
    state

    View full-size slide

  20. Allows you
    to go back in time

    View full-size slide

  21. Helps to understand the
    flow in your application

    View full-size slide

  22. Example:
    Discarding the
    customizations

    View full-size slide

  23. Keep track if channel was
    part of recommendation

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  28. Event sourcing

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. With the help of CQRS and
    Domain Driven Design

    View full-size slide

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

    View full-size slide

  35. Creating a new campaign

    View full-size slide

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

    View full-size slide

  37. 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
    );
    }

    View full-size slide

  38. 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
    );
    }

    View full-size slide

  39. 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
    );
    }

    View full-size slide

  40. 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);
    }

    View full-size slide

  41. 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);
    }

    View full-size slide

  42. 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);
    }

    View full-size slide

  43. 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;
    }

    View full-size slide

  44. 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;
    }

    View full-size slide

  45. 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;
    }

    View full-size slide

  46. 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;
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  50. // 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
    );
    }

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  54. 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);
    }

    View full-size slide

  55. 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);
    }

    View full-size slide

  56. 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);
    }

    View full-size slide

  57. 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);
    }

    View full-size slide

  58. It happened in the past

    View full-size slide

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

    View full-size slide

  60. How would we load a
    campaign?

    View full-size slide

  61. Campaign
    Event Store
    Campaign Created For
    Target Group

    View full-size slide

  62. // 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());
    }
    }

    View full-size slide

  63. // 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());
    }
    }

    View full-size slide

  64. // 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());
    }
    }

    View full-size slide

  65. // 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());
    }
    }

    View full-size slide

  66. Let's add a channel

    View full-size slide

  67. Campaign
    Event Store
    Add Channel To
    Campaign
    Channel Added To
    Campaign

    View full-size slide

  68. // 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);
    }

    View full-size slide

  69. // 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);
    }

    View full-size slide

  70. // 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);
    }

    View full-size slide

  71. // 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);
    }

    View full-size slide

  72. // 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
    ));
    }

    View full-size slide

  73. // 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
    ));
    }

    View full-size slide

  74. // 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
    ));
    }

    View full-size slide

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

    View full-size slide

  76. How do we get a listing
    of the campaigns?

    View full-size slide

  77. We'll create a read model

    View full-size slide

  78. Campaign
    Event Store
    Campaign Created For
    Target Group

    View full-size slide

  79. Campaign
    Event Store
    Campaign Created For
    Target Group
    Read Model
    Store

    View full-size slide

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

    View full-size slide

  81. 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);
    }

    View full-size slide

  82. 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()
    ));
    }

    View full-size slide

  83. 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()
    ));
    }

    View full-size slide

  84. 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()
    ));
    }

    View full-size slide

  85. 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);
    }

    View full-size slide

  86. 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);
    }

    View full-size slide

  87. 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);
    }

    View full-size slide

  88. Using the history to your
    advantage

    View full-size slide

  89. Discard Customizations

    View full-size slide

  90. // 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
    );
    }

    View full-size slide

  91. // 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']
    ));
    }

    View full-size slide

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

    View full-size slide

  93. Where are the tests?

    View full-size slide

  94. Scenario testing

    View full-size slide

  95. Given
    When
    Then

    View full-size slide

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

    View full-size slide

  97. $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 */)
    )
    ]);

    View full-size slide

  98. $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 */)
    )
    ]);

    View full-size slide

  99. $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 */)
    )
    ]);

    View full-size slide

  100. $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 */)
    )
    ]);

    View full-size slide

  101. 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 */)
    )
    ]);

    View full-size slide

  102. You can do something
    similar to test
    your read models

    View full-size slide

  103. Using the history to
    correct mistakes

    View full-size slide

  104. Replaying events to fix
    read models

    View full-size slide

  105. Replaying events to
    create new read models

    View full-size slide

  106. Very powerful

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  109. Find a solution for
    your specific problem

    View full-size slide

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

    View full-size slide

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

    View full-size slide