Dissecting an autonomous subsystem
At this point, we have divided our system into autonomous subsystems. Each subsystem is responsible to a single dominant actor who drives change. All subsystems are autonomous because they communicate via external domain events, and they are each housed in a separate cloud account that forms a natural bulkhead. This autonomy allows us to change the subsystems independently.
Now we are ready to start decomposing our subsystems into autonomous services. Again, the SRP plays a major role in defining the boundaries within a subsystem. First, we need to place a subsystem in context, then we will set up common components, and finally, we apply the major autonomous service patterns.
Context diagram
We will apply a set of autonomous service patterns to decompose a subsystem into services. These patterns cater to the needs of different categories of actors. So, we need to understand the context of an autonomous subsystem before we can decompose it into autonomous services. In other words, we need to know all the external actors that the subsystem will interact with. During event storming, we identified the behavior of the system and the users and external systems that are involved. Then we divided the system into autonomous subsystems so that each has a single axis of change.
A simple context diagram can go a long way to putting everyone on the same page regarding the scope of a subsystem. The context diagram enumerates all the subsystem’s external actors using yellow cards for users and pink cards for external systems. The diagram will contain a subset of the actors identified during event storming. We have encapsulated many of the original actors within other subsystems, so we will treat those subsystems as external systems. Figure 2.7 depicts the context of the Customer subsystem:
Figure 2.7: Subsystem context diagram
The Customer subsystem of our example Food Delivery System might have the following actors:
- The Customer will be the user of this subsystem and the dominant actor.
- The Restaurant Subsystem will publish external domain events regarding the restaurants and their menus.
- A Payment Processor must authorize the customer’s payment method.
- The subsystem will exchange OrderPlaced and OrderReceived external domain events with the Order Subsystem.
- The Delivery Subsystem will publish external domain events about the status of the order.
Now that the context is clear, we can start decomposing the system into its frontend, services, and common components.
Micro frontend
Each autonomous subsystem is responsible to a single primary user or a single cohesive group of users. These users will need a main entry point to access the functionality of the subsystem. Each subsystem will provide its own independent entry point so that it is not subject to the changing requirements of another subsystem.
The user interface will not be monolithic. We will implement the frontend using autonomous micro-apps that are independently deployed. The main entry point will act as a metadata-driven assembly and menu system. This will allow each micro-app to have a different reason to change and help ensure that the frontend is not responsible for increasing lead times and impeding innovation.
We will cover the frontend architecture in detail in Chapter 3, Taming the Presentation Tier.
Event hub
Each autonomous subsystem will contain its own independent event hub, as depicted in Figure 2.8, to support asynchronous inter-service communication between the autonomous services of the subsystem. Services will publish domain events to the event hub as their state changes. The event hub will receive incoming events on a bus. It will route all events to the event lake for storage in perpetuity, and it will route events to one or more channels for consumption by downstream services:
Figure 2.8: Event hub
We will cover the event hub in detail in Chapter 4, Trusting Facts and Eventual Consistency. In Chapter 7, Bridging Intersystem Gaps, we will cover how to bridge the event hubs of different subsystems together to create the event-first topology depicted in Figure 2.3.
Autonomous service patterns
There are three high-level autonomous service patterns that all our services will fall under as depicted in Figure 2.9. At the boundaries of our autonomous subsystems are the Backend For Frontend (BFF) and External Service Gateway (ESG) patterns. Between the boundary patterns lies the Control service pattern. Each of these patterns is responsible to a different kind of actor, and hence supports different types of changes:
Figure 2.9: Service patterns
Backend For Frontend
The Backend For Frontend (BFF) pattern works at the boundary of the system to support end users. Each BFF service supports a specific frontend micro-app, which supports a specific actor.
In the Customer subsystem of our example Food Delivery System, we might have BFFs to browse restaurants and view their menus, sign up and maintain account preferences, place orders, view the delivery status, and view order history. These BFFs typically account for about 40% of the services in a subsystem.
A listener function consumes domain events from the event hub and caches entities in materialized views that support queries. The synchronous API provides command and query operations that support the specific user interface. A trigger function reacts to the mutations caused by commands and produces domain events to the event hub.
We will cover this pattern in detail in Chapter 6, A Best Friend for the Frontend.
External Service Gateways
The External Service Gateway (ESG) pattern works at the boundary of the system to provide an anti-corruption layer that encapsulates the details of interacting with other systems, such as third-party, legacy, and sister subsystems. They act as a bridge to exchange events between the systems.
In the Customer subsystem of our example Food Delivery System, we might have ESGs to receive menus from the Restaurant subsystem, forward orders to the Order subsystem, and receive the delivery status from the Delivery subsystem. The Order subsystem would have ESGs that integrate with the various order systems used by restaurants. The Delivery subsystem would have an ESG to integrate with a push notifications provider. These ESGs typically account for upwards of 50% of the services in a subsystem.
An egress function consumes internal events from the event hub and then transforms and forwards the events out to the other system. An ingress function reacts to external events in another system and then transforms and forwards those events to the event hub.
We will cover this pattern in detail in Chapter 7, Bridging Intersystem Gaps.
Control services
The Control Service pattern helps minimize coupling between services by mediating the collaboration between boundary services. These services encapsulate the policies and rules that are governed by the business owners. They are completely asynchronous. They consume events, perform logic, and produce new events to record the results and trigger downstream processing.
We use these services to perform complex event processing and orchestrate business processes. They leverage the systemwide event sourcing pattern and rely on the ACID 2.0 properties (Associative, Commutative, Idempotent, and Distributed). In the Delivery subsystem of our example Food Delivery System, we might have a control service that implements a state machine to orchestrate the delivery process under many different circumstances. Control services typically account for about 10% of the services in a subsystem.
A listener function consumes lower-order events from the event hub and correlates and collates them in a micro events store. A trigger function applies rules to the correlated events and publishes higher-order events back to the event hub.
We will cover this pattern in detail in Chapter 8, Reacting to Events with More Events.
Now, let’s look at the anatomy of an autonomous service.