Reactive Architecture Foundations
Notes on reactive architecture for microservices
Updated: 03 September 2023
Foundations
Why Reactive
The primary goal of reactive architecture is to provide an architecture that is responsive under all conditions. Reactive architecrure is about solving business problems that are caused by technical challenges
Unresponsive Software
Unresponsive software can be an annoyance to users even if this is due to another system that you’re dependant on. Ideally we want to find ways that we can isolate ourselves from these external failures
In the case of certain organizations, an outage like this can be extremely costly in both monetary loss as well as customer loss
The Goal
How can we build software that:
- Scales to the necessery users
- Only consumes the necessary users
- Handle failures with little to no user impact
- Can be distributed over different machines
- Maintains a consistent level of responsivenes despite other factors
The Reactive Principles
The Reactive Manifesto is a set of patterns for solving some problems that companies were facing in a changing landscape
A reactive system has 4 main principles:
- Responsive - responds in a timely fashipn
- Resillient - remains responsive even when failures occur
- Elastic - remains responsive despite changes to the system load
- Message Driven - built on async, non-blocking messages
Responsive
“Responsiveness is the cornerstone of usability
Responsive systems build user confidence. In a reactive system all other principles are used to drive responsiveness
Resilient
“Resillience provides responsiveness despite failure”
Resillience is made possible by:
- Replication
- Isolation
- Containment
- Deligation
Failures should be isolated to a single component and recovery should be delegated to external components such as monitoring or lifecycle management components
It’s important that failures don’t bring down the entire system like it would in a monolith
Elastic
“Elasticity provides responsiveness, despite increases or decreases in load”
This implies that an architecture is able to both scale up or scale down in order to prevent bottlenecks as well as improve cost effectiveness
Message Driven
“Responsiveness, Resillience, and Elasticity are all supported by a Message Driven Architecture”
Messages should be async and non-blocking
Messages should provide:
- Loose coupling
- Isolation
- Location transparency
Additionally, this ensures that resources are not consumed while waiting as the application is not blocking or using threads while waiting for a response that may never come
Reactive Systems vs Reactive Programming
- Reactive systems are built in a way such that our individual application services and components interact in a reactive way (the above points). Reactive systems are separated along asynchronous boundries
- Reactive programming can be used to develop reactive systems makes use of async, often callback-based programming
The Actor Model
The Actor model is a programming paradigm that supports the development of reactive systems. The actor model is Message Driven and provides abstractions for Elasticity and Resillience
- All computation occurs inside an actor
- Each actor has an address
- Communication is done through async messages only
Location Transparency
The message driven design provides Location Transparency. All actors communicate using the same technique regardless of location. The original actor does not need to know the details of where another actor is located
The communication may be via a router that is able to direct messages as needed which also enables us to distribute actors
Reactive Systems without Actors
It’s possible to develop a Ractive system without the actor model using things like a:
- Service registry
- Load balancer
- Message bus
The result is a system that’s reactive at the architectural scale compared to the actor model in which individual components in the system are reactive in themselves which simplifies things as the development of the system grows
Domain Driven Design
One of the key goals of DDD is about establishing a communication channel between domain experts and the software
Large domains can be difficult to model, to better handle this we try to split domains into clearly differentiated sub-domains
DDD provides us with a set of guidelines for splitting these domains
What is a Domain
“A domain is a sphere of knowledge”
In software this refers to some business or idea that we are trying to model, the key goal of DDD is to build a model that domain experts understand
- The model represents our understanding of the domain
- The software is an implemetation of the model
Ubiquitous Language
This is a common language that is used to communicate between developers and domain experts, this language and terminology is driven by the domain experts and is based on words that come from the domain
It’s very important to ensure that the domain and software terminology is kept aligned
Decomponsing the Domain
Business domains are often large and complicated and which can contain many different ideas and actions with complex interactions. Trying to model a large domain can be complex, so it makes sense to split these domains into smaller subdomains for related parts of the domain
Some parts or ideas may exist in multiple subdomains and in each of these domains these may evolve separately so it is important to also realize that abstracting these things may not always be appropriate
Bounded Context
Each subdomain will have its own ubiquitous language. Often when building microservices we use Bounded Contexts as a starting point which we may break up further later on
Between different bounded contexts the same word may have very different meanings. For example an “order” may mean something very different to a waiter when compared to someone managing inventory
Some guidelines for identifying bounded contexts:
- Human culture and interaction - if different areas of the domain are handled by different people this may suggest a natural division
- Changes in Ubiquitous Language - if language changes meaning this may suggest a new context
- Varying levels of Detail - certain details may be relevant in one domain but irrelevant in another
Bounded contexts should be strongly separated and should result in smooth workflows. A context with too many dependencies may be overcomplicated
Traditional DDD focused on Objects, more modern DDD is focused on Events. This is also known as Event-First Domain Driven Design
The process of identifying these events is known as Event Storming
Domain Activities
When defining activities it can be useful to have a common notation to keep things clear, the subject-verb-object
notation is one method of doing this, an example of this would be ”Customer creates a booking”
- Customer - subject
- Creates - verb
- Booking - object
Sometimes there may also be a direct or indirect object, for example in an example like ”Host creates a booking for a customer” we have the following:
- Host - subject
- Creates - verb
- Booking - direct object
- Customer - indirect object
The indirect object is an object that’s related to the direct object
There are a few different types of activities in a domain:
Commands
- Represents a request to perform an activity
- Not yet happened, can be rejected
- Usually delivered to a specific location
- Causes a state change
Events
- Represent an action that happened in the past
- Can’t be rejected
- Often broadcast to many destinations
- Records a change to domain state, often the result of a command
- Always worded in past tense
Queries
- Request for information
- Expect a response
- Usually delivered to a specific destination
- Should not alter the state of the domain
In a reactive system, Commands, Events, and Queries are the types of messages in a reactive system and they form the API of a bounded context
Maintaining Purity
Once the domain has been split into bounded contexts it’s necessary to ensure those domains are maintined
Anti-Corruption Layers
Anti-Corruption layers help us to prevent details from one domain leaking into another
The ACL can be implemented as an abstract interface and there will then be an implementation that will translate this object between different domains
Sometimes Anti-Corruption systems are needed for interfacing with legacy systems that may not have good domain separation
Context Map
A context map is a way of visualizing bounded contexts as well as the relationships between them. The relationships/arrows between contexts indicate a dependency of some kind
Ubiquitous Language to Code
As we develop a bounded context we start translating commands into code, for example “Create a Booking” would translate to something like CreateBooking
Domain Objects
Value Object
- Defined by attributes
- Two objects are the same if their attributes are the same
- Immutable
- Messages in Reactive Systems are implemented as Value objects
Entities
- Identified by a unique identity
- May change attributes but not identity
- If identity changes then it is a new entity, regardless of attributes
Aggregates
- Collection of domain objects bound to an entity
- Objects in an aggregate can be treated as a single unit
- The root entity is called the Aggregate Root
- Access to objects in the Aggregate must go through the Aggregate Root
- Transactions should not span multiple Aggregate roots
- Aggregates are good candidates for distribution
Choosing an aggregate root is not always straightforward and can be different between different contexts. Some contexts may require multiple aggregates but this is not usually the case
Considerations when determining the Aggregate Root:
- Is the entity involved in most operations in the context
- Does deleting the entity require us to delete other entities
- Will a single transaction span multiple entities
Object-Field Notation
We use something like Order(orderId, orderItems, tableNumber, serverId)
as a way to identify th fields of an object
Domain Abstractions
Services
Some abstractions don’t necessarily fit with an Entity of Value Object
- Should be stateless
- Logic can be abstracted into services
- Can abstract away an anti-corruption layer
Services should be very thin layers over some business logic
Factories
Often there may be a lot of logic/complexity involved in creating a domain object
- Abstract away creation of domain objects
- Factories abstract away the creation logic
- May require access to external resources like databases, etc.
Repositiories
Repositories are a way of retreiving or modifying existing objects
- Used to read, udpdate, and delete existing objects
- Abstract access to external resources like databases, web services, etc.
- Allows you to switch in specific implementations when needed
Often we combine factories into repositories which then also handle the creation of objects as well
Hexagonal Architecture
Also known as “Ports and Adapters” and an alternative to the “N-Tier” architecture
- Domain is isolated to the center of the architectural focus
- Ports are exposed as APIs for the domain
- Adapters commuunicate with the domain through the port
- Can be viewed as an onion
- Outer layers depend on inner layers, inner layers do not depend on outer laers
- Ensures proper separation of domain from infrastructure
- Can be enforced with packages or projects
- Pieces of infrastructure can be swapped around without impacting domain, and vice versa
Since the domain has no dependency on the other parts of the architecture it can be portable
Reactive Microservices
Monoliths
The worst-case for a monolith can often have:
- No clear isolation
- Complex dependencies
- Hard to understand and modify
To clean up a an application like above we would often split the application into a few separate domain boundries
Characteristics
- Deployed as a single application
- Single, shared database
- Communicate with synchronous method calls
- Deep coupling between components, often through the DB
- Big releases
- Long development cycles (weeks or months)
- Careful synchronization of features and releases needed
Scaling a monolith involves multiple instances of the same application with a shared database
Advantages
- Easy cross-module refactoring
- Easier to maintain consistency
- Single deployment process
- Snigle application to monitor
- Simple scaling model
Disadvantges
- Limited by the max size of a single server
- Only scales within the limitations of a database
- Components must all be scaled together
- Deep coupling, inflexibility
- Slow development cycles
- a Serious failure can bring down the entire monolith and can potentially cascade through instances
Service Oriented Architecrure
Not necessarily the same as microservices
The introduction of isolation can be done by way of a service-oriented-architecture
In an SOA each individual service:
- Has its own database
- All access goes through the service’s API
- Services can live in a monolith or as a set of microservices
Microservices
- Subset of SOA
- Logical components are separated into services
- Should be deployed independently
- Each component has its own data store
- Independent and self governing
Characteristics
- Deployed independently
- Multiple independent databases
- Synchronous or asynchronous communication
- Loose coupling
- Rapid deployments
- Teams manage features on their own
- Teams use a DevOps approach
Scaling
- Each service scales independenntly
- Can be one or multiple copies of each service per server
- Each server hosts a subset of the entire system
Advantages
- Individual services can be deployed and scaled
- Increased availabilities, more isolated failures
- Isolation and decoupling creates more flexibility
- Supports multiple platforms and languages
Team Organization
- Team independence
- Faster release cycles
- Cross-team coordination is less necessary
- Can increase productivity
Disadvantages
- May require complex deployment and monitoring systems
- Cross-service refactoring is complicated
- Requires supporting older API versions
- May require organizational changes
Responsibilities of Microservices
Single Responsibility Principle
“A class should have only one reason to change”
The SRP can also be applied to microservices. A micorservice should only have a single responsibility and a change to the internals of one microservice should not necessitate changes to other services
Bounded Contexts
Bounded contexts are a good starting point for the division of individual services. These define a context in which a specific model applies
After the initial definition, services may additionally be subdivided within a bounded context
Principles of Isolation
- State
- Space
- Time
- Failure
State
Isolation of state is accomplished by ensuring all state access goes through the API, this allows internal evolution of the service while maintaining the internal api
Space
Microservices should not care where other services are deployed. This allows the service to be scaled up or down as needed
Time
Microservices should not wait for each other. Requests should be async and non-blocking which enables us to free up additional resource time
This enables eventual consistency which means that now services can sale in independently
Failure
A service’s failure should not cause failure in other services. This enables our system to remain operational even if some parts are failing
Bulkheading
Bulkheading is a technique for isolating failures
- Create failure zones in an application
- Prevent failure propogarion
- Overall system can remain operational
Circuit Breaker
When a service is overloaded a caller may not realize the service is overloaded and the retries may lead to further failure
- Avoid overloading a service
- Quarantine a failing service so it can fail fast
- Allows failing service time to recover without overloading
In the event of a failure the circuit breaker is failed fast and this causes messages to go through the circuit breaker instead of to the service itself
Over time the circuit breaker will allow a single request through to basically test if the service is back up, if the service is up then the circuit breaker switches back to the closed state
States:
- Closed - normal operation, service is acessible
- Open - once tripped by some error state, this will prevent requests from going to the service to allow it time to recover
- Half Open - after some time of being close the circuit breaker will switch to this state, this will allow one request to go through to check if the service is operational, if it works then the the service will become available again and the circuit breaker will Reset itself back to the Closed state. If the request does not work it will again go back to the Open state
Message Driven Architecture
Reactive systems are based on messaging
- Aysnc, non-blocking - decouples by time and faiulure
- Services are not dependent on responses from each other
- If a request fails, the failure won’t propogate
- The client isn’t waiting for a response
Autonomy
Each service should be able to operate independently
- Microservices can only gaarantee their own behaviour
- Isolation allows a service to operate independently
- Each service can be autonomous
- Automnomous services have enough information to resolve conflicts and repair failures
- Don’t require other services to be operational all the time
Benefits
- Stronger scalability and availability
- Scaled indefinitely
- Operate indepentently means that services are more failure tolerant
Implementation
- Communicate through async messages
- Maintain enough internal state for isolated functioning
- Use Eventual Consistency
- Avoid direct, synchronous dependencies on external services
Gateway Services
Microservices can lead to complexities in the API
- Data may be distributed between services
- Clients may need to aggregate data
- Client may need to know about all services
- Clients must manage complex aggregations and failure handling
API Gateway Services are services between microservices and clients
- Gateway sents requests to individual services for aggregation
- Logic and aggregation happens in the Gateway service
- Gateway handles failures from each service, client only needs to handle gateway failures