Example – Author BFF
In Chapter 2, The Anatomy of Cloud Native Systems, we discussed how the data life cycle is a useful strategy for decomposing a system into bounded isolated components. As the data in the system ages, its storage requirements will change and thus the component that owns the storage changes as well. The users who interact with the data will also change over the data life cycle and thus so do the frontend applications used over the data life cycle. In a reactive, cloud-native system, a great deal of data is automatically created as events flow through the system. However, an event chain has to start somewhere. If it doesn't start in an external system, then it starts when a user creates it. This is where the Author user experience comes into play. Examples could include any data entry intensive user interaction, such as authoring content for an e-commerce site, creating records in a case management system, managing reference data in virtually any system, and so on.
As depicted in the following figure, an Authoring BFF component generally consists of two out of the three APIs in the Trilateral API pattern. An Authoring BFF has a synchronous API with commands and queries to create and retrieve domain entity data and an asynchronous API to publish events as the state of the entities change. The API Gateway pattern is leveraged for the synchronous interface, the Cloud Native Databases per Component pattern is followed for storing the data in the most appropriate polyglot persistence, and the Database-First Event Sourcing pattern variant is utilized to publish events.
A RESTful CRUD implementation of the synchronous interface is suitable for a simple domain model. However, a GraphQL implementation is a good choice for more complex domain models, as it provides for a very structured implementation pattern. We will discuss this in more detail shortly. GraphQL is also client-oriented in that the client directs the format of query responses and the invocation of mutations. The server-side logic naturally conforms to Postel's Law (aka the Robustness Principle): be conservative in what you send, be liberal in what you accept. Thus, GraphQL is also a good choice if users author the data across multiple device types. For example, a case agent might enter essential information while on the go and double back later to complete the remaining details from a desktop.
Each aggregate domain entity is typically stored as a JSON document in a cloud-native document store, such as AWS DynamoDB, with references to related aggregates in other tables. GraphQL excels at stitching these relationships together for query responses and when publishing events. The published events consist of created, updated, and deleted events for each aggregate domain with the before and after snapshot of the instance. There are usually one or more primary aggregates that are the focal point of the business activity. These will contain a status field that is used to publish more concrete domain events, such as submitted, approved, rejected, completed, and so forth. A Search BFF component usually accompanies an Authoring BFF. The Search BFF implements the CQRS pattern to react to the events of the Authoring BFF and store the aggregate domain entities in a search engine.
A question often arises regarding the Authoring BFF and the single responsibility principle. There is a tendency to create a component per aggregate domain entity. This can lead to a lot of small components that must interact at the relationships between the aggregates, either via synchronous inter-component communication or via data synchronization. Of these choices, we definitely prefer data synchronization. However, the value of all these smaller components is limited at best, because our aim is to avoid shared components and synchronous inter-component communication in favor of turning the database inside out with materialized views.
Instead, the act of authoring is the single responsibility. It is perfectly reasonable to author all the domain entities of a complex domain model within a single component provided that the team can continuously deliver changes to the component with confidence. To achieve this confidence, the context of the domain model must certainly be bounded and cohesive. The code for the component must also follow a very predictable pattern. This is where the structure of GraphQL code also excels. Following best practices, the code for each domain entity in a GraphQL schema has the same structure: schema fragment, resolvers fragment, model class, validation rules, and pluggable connectors. The following is a typical schema fragment.
export const schema = `
type Item {
id: ID
name: String!
description: String
}
extend type Query {
item(id: String!): Item
itemsByName(name: String!, limit: Int, cursor: String): [Item]
}
extend type Mutation {
saveItem(
input: Item
): Item
deleteItem(
id: ID!
): Item
}
`;
It is very easy to reason about the logic of many domain entities in a single Authoring BFF because the code follows a predictable pattern and it is solely focused on the act of data entry. Deviations from the pattern are quickly spotted in the code review. A Search BFF is implemented as a separate component precisely because its code follows a different pattern. Thus a single search BFF can support multiple domain entities as well. Still, all of this is cohesive within a bounded context and the user activity is scoped to a particular user base. In the following user experience categories, we will see where other responsibilities reside.