A Broadway demo application


After the release of Broadway we got a lot of requests for a demo application. People were curious how we use Broadway and how everything ties together.

So Fritsjan started with the demo application a while back and we just published it on Github. In this blog post we want to take a closer look at the demo application.

The demo itself

The demo we created is based on the domain examples that Mathias Verraes uses in his talk about Practical Event Sourcing. There he models a webshop using event sourcing. You can pick up a basket, add products to it and checkout the basket. During checkout we keep track of the items that are bought together.

If you like, you can try the demo by accessing the API, for example via curl. The application has no GUI.

In the API we have the following requests:

Method Path Description
POST /basket Pickup a new basket, returns the basketId
POST /basket/{basketId}/addProduct Add a product to a basket (productId and productName should be given as form fields)
POST /basket/{basketId}/removeProduct Remove a product from a basket (productId as form field)
POST /basket/{basketId}/checkout Checkout a basket
GET /advice/{productId} Retrieve Other people also bought this list

Installation

To install the demo application, simply clone the repository, install the dependencies and run vagrant up.

$ git clone https://github.com/qandidate-labs/broadway-demo.git
$ cd broadway-demo
$ curl -sS https://getcomposer.org/installer | php
$ php composer.phar install
$ vagrant up # uses ansible to provision the machine

The domain

You can find the domain objects in src/BroadwayDemo/Basket, with the aggregate root Basket. The controllers and service definition can be found in src/BroadwayDemoBundle.

In this demo application the basic flow is as follows:

A request is handled by the controller, from the controller we dispatch a Command to the commandbus. The BasketCommandHandler handles the command and interacts with Basket by creating a new basket, or by loading an event stream to recreate the basket and calling a method on it. The basket then dispatches a new event to record what actually happened. That event is then applied on Basket itself where it then updates its internal state.

During a /checkout request, there is an event that is also applied on the PeopleThatBoughtThisProductAlsoBoughtProjector. That projector is responsible for keeping track of the bought combinations and stores it in the (Elasticsearch) repository.

Sure, but how does it work?

Most of the configuration is just framework glue using the Bundle provided with Broadway. In src/BroadwayDemoBundle/Resources/config/ you will find the main configuration files. Most important for this demo is domain.xml, in that file we actually configure the parts to bind all Broadway components together. Let's take a look at each service.

<service id="broadway_demo.basket.repository" class="BroadwayDemo\Basket\BasketRepository">
    <argument type="service" id="broadway.event_store" />
    <argument type="service" id="broadway.event_handling.event_bus" />
    <argument type="collection">
        <argument type="service" id="broadway.metadata_enriching_event_stream_decorator" />
    </argument>
</service>

This service is the aggregate root repository for the Basket. The repository has 3 arguments, the first is the event store, it's a service that is defined in the Bundle provided with Broadway. The second argument is the event bus, a class that dispatches events to registered listeners. The last argument is an array of decorators that can be used to enrich events before they get saved in the event store.

<service id="broadway_demo.basket.command_handler" class="BroadwayDemo\Basket\BasketCommandHandler">
    <argument type="service" id="broadway_demo.basket.repository" />
    <tag name="command_handler"/>
</service>

Here we configure the command handler, by passing in the repository we configured before. By tagging the service with command_handler, the service gets registered with the CommandBus which is configured in the Broadway Bundle.

<service id="broadway_demo.read_model.repository.people_that_bought_this_product"
         factory-service="broadway.read_model.repository_factory"
         factory-method="create"
         class="Broadway\ReadModel\ReadModel">
    <argument>broadway_demo.people_that_bought_this_product</argument>
    <argument>BroadwayDemo\ReadModel\PeopleThatBoughtThisProductAlsoBought</argument>
</service>

This service is a read model repository, in this case an Elasticsearch repository.

<service id="broadway_demo.read_model.projector.people_that_bought_this_product"
         class="BroadwayDemo\ReadModel\PeopleThatBoughtThisProductAlsoBoughtProjector">
    <argument type="service" id="broadway_demo.read_model.repository.people_that_bought_this_product" />
    <tag name="broadway.domain.event_listener" />
</service>

The last service in the file, is the projector. This projector is used to listen to the BasketCheckedOut event. We pass it the previously configured repository and tag it with broadway.domain.event_listener so it gets subscribed to the event bus configured in the Broadway Bundle.

Testing

Naturally the demo also contains tests. The most interesting tests are probably the CommandHandler tests and the ProjectorScenario tests.

Lets take a look at the AddProductToBasketTest:

public function it_adds_a_product_to_a_basket()
{
    // ..
    $this->scenario
        ->withAggregateId($basketId)
        ->given(array(new BasketWasPickedUp($basketId)))
        ->when(new AddProductToBasket($basketId, $productId, $productName))
        ->then(array(
            new ProductWasAddedToBasket($basketId, $productId, $productName)
        ));
}

The scenario is a helper to write clear tests with 3 main methods: given some events happened, when I dispatch given command, then I expect these (extra) events to have been recorded. By using withAggregateId the scenario knows which id to use when storing the events. The then actually does some asserts to match the recorded events.

The PeopleThatBoughtThisProductAlsoBoughtProjectorTest does a similar thing, but instead of testing against events, it tests against created read models.

But there is more!

This demo doesn't include all features Broadway offers, but we hope it does give a good impression how you can use Broadway. If you use Broadway in a different way, we would be interested to hear about it! Ping us in #qandidate on Freenode, or mention us on Twitter.