Application =========== In this tutorial we will take a look at event-sourced application in more detail. When you have your domain model implemented with event-sourced aggregates, you need to somehow trigger the business logic inside them and interact with other aggregates. Despite the fact that you can use aggregates directly, it is not advised. Usually, you have a set of use cases in your business which start a process that involves one or multiple aggregates and implementation of these use cases is where an application steps in. For that purpose, you use `Application` class. .. code-block:: python from bestagon.core.event_processor import Applicaiton An application serves multiple purposes: - It has its own domain model. - It includes use cases to work with domain model. - It contains a persistence mechanism, able to store and retrieve event-sourced aggregates. - It contains a mechanism to react to events that have been produced by your domain model. You can think of an application like a microservice inside a microservice - it is a boundary with strict borders where each application has its own domain model, knows nothing about other applications, but can react to events generated by other domain models. Application name ---------------- Before we dive into implementation of use cases and reaction to events, we should address some technical details regarding the Application class. The first one is `get_name` method. Similar to `Aggregate` which should have a unique ID, your applications should also have a unique identifier - its name. It is used internally by the system and **should not be changed** after you define it, because it can corrupt your system. .. code-block:: python class BondsApplication(Application): def get_name(self) -> str: return 'bonds_application' # Unique across your system Event-sourced repository ------------------------ These are technical details for those who want to get a better understanding of how the framework works internally. In most cases, you do not need to reimplement the constructor of the application, and you can concentrate on writing business logic. When you initialize your application, it takes two arguments as input - `repository` and checkpoint_store`: .. code-block:: python class BondsApplication(Application): def __init__(self, repository: EventSourcedRepository, checkpoint_store: CheckpointStore): # Initialization logic is here Repository is a subclass of `EventSourcedRepository` class. This component plays a crucial part in the application and implements a persistence mechanism inside your application that allows you to save and retrieve aggregates. It hides the event store and persistence mechanism behind the simple interface: - `get_by_id` method allows you to retreive stored aggregate from event store. - `save` method saves the aggregate in event store. - `contains` method allows you to check whether aggregate exists in repository. **IMPORTANT** - all these methods are asynchronous and should be used with async / await syntax. You can access repository inside your application at any time using `repository` property. Checkpoint store ---------------- Another input parameter is a `checkpoint_store`. Each application contains a subscription to the event store, reads events from it one by one and reacts in a specific way. Such an approach allows you to automatically react to events and automate the execution of use cases, but it requires a mechanism to save information which events have already been processed. For that purpose, a checkpoint store has been designed. It is a subclass of the abstract `CheckpointStore` class with a simple interface. It simply stores the position of the last processed event in the event store, and if the system is restarted or crashed, it will be able to continue its work after the last processed event. **WARNING** - checkpoint store uses the application name as a key to the checkpoint and if you change the application name after you have already started to process events **it will break your application** because it will not be able to find the checkpoint for that name, will create a new one and start to process events that have already been processed. Defining use cases ------------------ Now it is time to write use cases. Similar to `Aggregate` there are two types of methods - command handlers and event handlers. Command handlers are your actual use cases - public methods with names that should reflect your business domain, and they receive one and only one parameter as input - a command. Inside a command handler, you create new aggregates, retrieve already existing from the repository, execute business logic inside aggregates and orchestrate communication between multiple aggregates. Command handlers return nothing, their responsibility is to execute business logic and save changes in the aggregates in the repository. Here is a simple example - for our application to work we need to be able to issue new bonds: .. code-block:: python class BondsApplication(Application): # Command handler name reflects you business domain and it receives a Command as input parameter async def issue_bond(self, command: IssueBond) -> None: # Here you can add a validation logic for your command values # If the data inside the command is corrupted, then use case should fail with exception # Before creating new aggregate we check for duplicates and return if this bond have already been issued aggregate_id = Bond.create_id(isin=command.isin) if await self.repository.contains(aggregate_type=Bond.get_aggregate_type(), aggregate_id=aggregate_id): logger.info(f'Duplicate bond {command.isin}') return # Create new aggregate and save it in repository bond = Bond.issue(command) await self.repository.save(bond) **NOTE** - command handlers are asynchronous functions. **IMPORTANT** - when you write your use cases, it is highly advisable to stick to the rule of thumb - **modify only one aggregate at a time**. Situations when you need to modify multiple aggregates at once show you weak points in your design, and it is a signal that you need to return to the design of your domain model. In addition to command handlers, it is good practice to add getters to your application to retrieve aggregates from repository, this will allow you to concentrate on your business logic instead of the technical details of a framework. .. code-block:: python async def get_bond(self, isin: str) -> Bond: aggregate_id = Bond.create_id(isin) bond = await self.repository.get_by_id(aggregate_type=Bond.get_aggregate_type(), aggregate_id=aggregate_id) return bond Let's add another couple of use cases to the application. We need to be able to update term to maturity of the bond and set the bond as matured: .. code-block:: python async def set_matured(self, command: SetMatured) -> None: bond = await self.get_bond(isin=command.isin) if bond.ready_to_mature(): bond.set_matured() await self.repository.save(bond) async def update_term_to_maturity(self, command: UpodateTermToMaturity) -> None: # First get bond from repository bond = await self.get_bond(isin=command.isin) # Execute business logic in your aggregate bond.update_term_to_maturity() # Save changes in repository await self.repository.save(bond) Reaction to events ------------------ Bestagon provides a mechanism of reaction to the events generated by applications. It allows you to trigger use cases automatically as a reaction to a specific event. To benefit from this mechanism, you need three things: command handler, event handler and event routing. We have already defined several command handlers for our application. Now let's take a look at event routing. An event routing is a simple dictionary where the keys are event types you want to react to and values are corresponding event handlers. To provide event routing for your application, you need to reimplement `get_event_routing` abstract method: .. code-block:: python def get_event_routing() -> dict: routing = { Bond.Issued: self._when_bond_issued, Bond.TermToMaturityChanged: self._when_term_to_maturity_changed, } return routing Here we provide our application with reactions to the events - what happens when the bond is issued and its term to maturity changed. **IMPORTANT** - here we react to local events that have been generated by domain model in the application, but you can also react to events from other applications simply by adding them to event routing. The private methods in the event routing are event handlers. They are asynchronous methods that receive an event as an input parameter and trigger a specific use case. You do not implement business logic inside an event handler. Its main goal is to take an event and trigger a corresponding use case, so it is good practice to make them as thin as possible: .. code-block:: python async def _when_bond_issued(self, event: Bond.Issued) -> None: command = UpdateTermToMaturity(isin=event.isin) await self.update_term_to_maturity(command) async def _when_term_to_maturity_changed(self, event: Bond.TermToMaturityChanged) -> None: command = SetMatured(isin=event.isin) await self.set_matured(command) Now you know how to write use cases and react to events using `Application` class. In the next tutorial you will learn how Bestagon implements CQRS pattern to create read models with `Projection` class.