Bounded contexts with Axon
"A Context is the setting in which a word or statement appears, and that determines its meaning".
A Bounded Context is an explicit boundary within which a domain model exists. The domain model expresses a Ubiquitous Language as a software model. When starting with software modeling, Bounded Contexts are conceptual and are part of the 'problem space'. In this phase, the task is to find the actual boundaries of specific contexts and then visualize the relationships between these contexts. As the model starts to take on a deeper meaning and clarity, Bounded Contexts will transition to the 'solution space', with the software model being reflected in project source code.
Bounded Contexts represent logical boundaries from a run-time perspective, defined by contracts within software artifacts where the model is implemented. In Axon applications, the contract (API) is represented as a set of messages (commands, events, and queries) that the application publishes and consumes. Bounded Contexts are a strategic concept in Domain Driven Design, and it is important to know how it is reflected in the architecture and organizational/team structure.
Let’s introduce a sample subdomain of Shipping management responsible for managing courier information and contains a courier view of an order (shipping) for managing the delivery of orders.
In Axon applications, the CQRS architectural pattern decouples command components from the query side components. Additionally, these components communicate via messages (commands, events, queries) transparently. These components aren’t interested in the actual destination of a message. It’s simply a matter of configuration whether the system runs on a single node or is distributed on several nodes.
This design enables different deployment strategies with different scalability options, for example:
- deploy all components within one service
- deploy command and query components separately (CQRS), as two independent services
- deploy command and query components as two independent services (CQRS), and extract their HTTP/REST adapters/controllers to services as well (location transparency)
The core of the service is its business logic, which is surrounded by adapters that communicate with other services and applications. It has a hexagonal architecture:
The API of this service is represented as a set of commands and queries it consumes and events it publishes. A layer of adapters (REST, WebSockets, gRPC, ...) maps the 'user-friendly' API to the core messaging API of the service.
Shipping-Query and Shipping-Command services
By applying CQRS architectural pattern, the system can be further decoupled by separating the command component from the query component of the Shipping service and deploying them individually as two services:
The Shipping-Command service consumes commands only. The commands are handled by the business logic (aggregates), and domain events are published. The Shipping-Query service is subscribed to these domain events, and it creates projections (materialized views) of the system’s aggregates. These projections are optimized for querying, and the Shipping-Query service exposes 'queries' as an API.
Shipping-Query, Shipping-Command, and Adapters as services
Location transparency allows for additional extraction of REST/HTTP adapters as services and deploys them individually. This has enormous benefits for operational teams allowing them to update one component without making other components unavailable.
These diagrams show the Hexagonal (Ports & Adapters) architecture of three different options, with the service contracts (API) published as a schema. This generally means that if the events/commands/queries are published as JSON, or perhaps a more economical object format, the consumer can consume the messages by parsing them to obtain their data attributes.
The logical boundary of the Shipping context is represented as a set of messages we choose to consume (commands, queries) or publish (events). Inter-service communication is performed via gRPC through Axon Server, an infrastructure component capable of routing these messages to interested services.
Whatever the choice of deployment, these services will speak the same language and belong to the same bounded context.
Inverting Conway’s Law allows for the organizational structure to align with the bounded contexts.
“Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.”
Therefore, there are several rules that should be followed:
- Explicitly set boundaries in terms of team organization.
- Keep the model strictly consistent within these bounds, and don’t be distracted or confused by issues outside.
- Ideally, keep one subdomain model per Bounded Context.
There should be a single team assigned to work on one Bounded Context. There should also be a separate source code repository for each Bounded Context. It is possible that one team could work on multiple Bounded Contexts, but multiple teams should not work on a single Bounded Context.
A bounded context never lives entirely on its own. Information from different contexts will eventually be synchronized. It is useful to model this interaction explicitly. Domain-Driven Design names a few relationships between contexts, which drive the way they interact:
- partnership (two contexts/teams combine efforts to build interaction)
- customer-supplier (two contexts/teams in upstream/downstream relationship - upstream can succeed independently of downstream contexts)
- conformist (two contexts/teams in upstream/downstream relationship - upstream have no motivation to provide to downstream, and downstream context does not put the effort in translation)
- shared kernel (explicitly, sharing a part of the model)
- separate ways (cut them loose)
- anti-corruption layer (the downstream context/team builds a layer to prevent upstream design to 'leak' into their own models by transforming interactions)
Let’s introduce the second sample subdomain:
Order management (Order taking and fulfillment process)
The Order and Shipping aggregate class in each subdomain model represents a different term of the same 'Order' business concept. Shipping’s version of an Order consists of status and address, which tell the courier how and where to deliver the order.
These two contexts are in the upstream-downstream relationship where the Order service (downstream) depends on the API of the Shipping service (upstream). For example, the Order service is responsible for the order fulfillment process, and it will trigger a 'command' to the Shipping service to create a Shipping ‘Order’. Once the courier delivers the shipping/order, The Order service will receive an event from the Shipping service and continue with the order fulfillment process. It is important to note that the Order service has a dependency on the Shipping service and not the other way around. If restricted to use events only (no commands or queries), the services would become programmatically interdependent, introducing tight coupling.
More specifically, these two contexts are in the customer-supplier relationship.
To align with Convoy’s law, we should organize two teams to produce these bounded contexts.
The teams define automated acceptance tests that validate the interface the upstream team provides. The upstream team can then make changes to their code without fear of breaking something downstream. This is where a Consumer-Driven Contract test comes into play. This test is part of our domain model, reflecting the consumer-supplier relationship in the source code.
When two contexts with an upstream/downstream relationship are not in a cooperative environment, a pattern such as customer-supplier will not work. In this case, the downstream team builds an anti-corruption layer (independently deployable Axon application/component) to prevent the upstream design from 'leaking' into their own models by transforming interactions.
Applying concepts from Domain-Driven Design will enable us to design our domain model effectively.
Axon deals with Bounded Contexts in a few different ways. From the Axon Framework perspective, by separating business logic from the configuration. This allows logic to focus on the relevant aspect of the context itself by using the configuration of serializers, upcasters, etc., to explicitly define how messages and interactions are shared beyond the boundaries of the context.
Additionally, Axon Server (Enterprise) explicitly supports bounded contexts by allowing different (groups of) applications to connect to different contexts within the Axon Server. Unless specifically indicated otherwise, contexts are strictly separated, and information/messages are not shared between them.
Axon Framework is an open-source Java framework for event-driven Microservices and Domain Driven Design. Axon Server is a zero-configuration message router and event store for Axon-based applications. To learn more about Axon Server Enterprise, check out this page or get in touch with us. You can download our free quick-start package here to get you up and running.
Solutions architect with significant experience in designing full stack application components and providing guidance to the solutions teams in development and implementation.
Skilled in a wide variety of technology stacks and learning quickly new technologies as needed. Experience covers all facets of design patterns, software architecture, continuous delivery, agile methodologies and best practices in constructing solutions that remain scalable, adaptable and replicable. Strong engineering professional with the Master of Science (MSc) focused on Computer Science from the University of Belgrade, Faculty of Mathematics.
September 28th, Amsterdam
Join us for the AxonIQ Conference 2023, where the developer community attends to get inspired by curated talks and networking.
September 27th, Amsterdam
The event to collaborate, discuss, and share knowledge about techniques, tools, and practices for building complex, distributed event-driven applications.