# - Software Architecture Patterns Second Edition 9781098134273
# - Software Architecture Patterns Second Edition 9781098134273
SECOND EDITION
Mark Richards
Software Architecture Patterns
by Mark Richards
Copyright © 2022 O’Reilly Media, Inc. All rights reserved.
Printed in the United States of America.
Published by O’Reilly Media, Inc., 1005 Gravenstein Highway North,
Sebastopol, CA 95472.
O’Reilly books may be purchased for educational, business, or sales promotional
use. Online editions are also available for most titles (http://oreilly.com). For
more information, contact our corporate/institutional sales department: 800-998-
9938 or [email protected].
It’s all too common for developers to start coding an application without a
formal architecture in place. This practice usually results in ill-defined
components, creating what is commonly referred to as a big ball of mud. These
architectures are generally tightly coupled, brittle, difficult to change, and lack a
clear vision or direction. It’s also very difficult to determine the architectural
characteristics of applications lacking a well-defined architectural style. Does the
architecture scale? What are the performance characteristics of the application?
How easy is it to change the application or add new features? How responsive is
the architecture?
Architecture styles help define the basic characteristics and behavior of an
application. Some architecture styles naturally lend themselves toward highly
scalable systems, whereas other architecture styles naturally lend themselves
toward applications that allow developers to respond quickly to change.
Knowing the characteristics, strengths, and weaknesses of each architecture style
is necessary to choose the one that meets your specific business needs and goals.
A lot has happened in software architecture since 2015 when the first edition of
this report was published. Both microservices and event-driven architecture have
gained in popularity, and developers and architects have found new techniques,
tools, and ways of designing and implementing these architecture styles. Also,
the widespread use of domain-driven design has led to a better understanding of
how architectures are structurally partitioned, and how that partitioning can
impact the design and implementation of a system. The second edition of the
report addresses both of these advances.
The second edition also includes other significant enhancements, along with
more information about the intersection of architecture and data, and an
expanded analysis section at the end of each chapter. These new sections provide
you with better guidelines for when to use (and not to use) each architecture
presented in this report.
Another change you’ll notice in the second edition is the use of the term
architecture style rather than architecture pattern for the architectures described
in this report. This distinction helps alleviate some of the confusion surrounding
the differences between, say, event-driven architecture—an architecture style—
and something like CQRS (Command Query Responsibility Segregation), which
is an architecture pattern.
An architecture style, such as the ones presented in this report, describe the
macro structure of a system. Architecture patterns, on the other hand, describe
reusable structural building block patterns that can be used within each of the
architecture styles to solve a particular problem. Take, for example, the well
known CQRS pattern, which describes the structural separation between read
and write operations to a database or eventing system (for example, separate
services and databases for read operations and write operations). This
architecture pattern could be applied to any of the architecture styles described in
this report to optimize database queries and updates.
Architecture patterns, in turn, differ from design patterns (such as the Builder
design pattern) in that an architecture pattern impacts the structural aspect of a
system, whereas a design pattern impacts how the source code is designed. For
example, you can use the Builder design pattern as a way to implement the
CQRS architecture pattern, and then use the CQRS pattern as a building block
within a microservices architecture. Figure 1-1 shows this hierarchical
relationship among the three terms and how they interrelate with each other to
build software systems.
Figure 1-1. Architecture styles can be composed of architecture patterns, which in turn can be composed of
design patterns
Architecture styles allow you to use existing and well-known structures that
support certain architectural characteristics (also known as nonfunctional quality
attributes, system quality attributes, or “-ilities”). They not only provide you
with a head start on defining an architecture for a given system, but they also
facilitate communication among developers, architects, quality assurance testers,
operations experts, and even in some cases, business stakeholders.
Architecture Classification
Architecture styles are classified as belonging to one of two main architectural
structures: monolithic (single deployment unit) and distributed (multiple
deployment units, usually consisting of services). This classification is important
to understand because as a group, distributed architectures support much
different architecture characteristics than monolithic ones. Knowing which
classification of architecture to use is the first step in selecting the right
architecture for your business problem.
Monolithic Architectures
Monolithic architecture styles (as illustrated in Figure 2-1) are generally much
simpler than distributed ones, and as such are easier to design and implement.
These single deployment unit applications are fairly inexpensive from an overall
cost standpoint. Furthermore, most applications architected using a monolithic
architecture style can be developed and deployed much more quickly than
distributed ones.
Figure 2-1. Monolithic architectures are single deployment units
While cost and simplicity are the main strong points of a monolithic architecture,
operational characteristics such as scalability, fault tolerance, and elasticity are
its weak points. A fatal error (such as an out of memory condition) in a
monolithic architecture causes all of the functionality to fail. Furthermore, mean
time to recovery (MTTR) and mean time to start (MTTS) are usually measured
in minutes, meaning that once a failure does occur, it takes a long time for the
application to start back up. These long startup times also impact scalability and
elasticity. While scalability can sometimes be achieved through load balancing
multiple instances of the application, the entire application functionality must
scale, even if only a small portion of the overall application needs to scale. This
is not only inefficient, but unnecessarily costly as well.
Examples of monolithic architecture styles include the layered architecture
(described in Chapter 3), the modular monolith, the pipeline architecture, and the
microkernel architecture (described in Chapter 4).
Distributed Architectures
As the name suggests, distributed architectures consist of multiple deployment
units that work together to perform some sort of cohesive business function. In
today’s world, most distributed architectures consist of services, although each
distributed architecture style has its own unique formal name for a service.
Figure 2-2 illustrates a typical distributed architecture.
Figure 2-2. Distributed architectures consist of multiple deployment units
Architecture Partitioning
Besides being classified as either monolithic or distributed, architectures can
also be classified by the way the overall structure of the system is partitioned.
Architectures, whether they are monolithic or distributed, can be either
technically partitioned or domain partitioned. The following sections describe
the differences between these partitioning structures and why it’s important to
understand them.
Technical Partitioning
Technically partitioned architectures have the components of the system
organized by technical usage. The classic example of a technically partitioned
architecture is the layered (n-tiered) architecture style (see Chapter 3). In this
architecture style, components are organized by technical layers; for example,
presentation components that have to do with the user interface, business layer
components that have to do with business rules and core processing, persistence
layer components that interact with the database, and the database layer
containing the data for the system.
Notice in Figure 2-3 that the components of any given domain are spread across
all of these technical layers. For example, the customer domain functionality
resides in the presentation layer as customer screens, the business layer as
customer logic, the presentation layer as customer queries, and the database
layer as customer tables. Manifested as namespaces, these components would be
organized as follows: app.presentation.customer,
app.business.customer, app.persistence.customer, and so on.
Notice how the second node in the namespace specifies the technical layering,
and that the customer node is spread across those layers.
Figure 2-3. In a technically partitioned architecture, components are grouped by their technical usage
Domain Partitioning
Unlike technically partitioned architectures, components in domain partitioned
architectures are organized by domain areas, not technical usage. This means
that all of the functionality (presentation, business logic, and persistence logic) is
grouped together for each domain and subdomain area in separate areas of the
application. For domain partitioned architectures, components might be
manifested through a namespace structure such as app.customer,
app.shipping, app.payment, and so on. Notice that the second node
represents the domain rather than a technical layer. As a matter of fact, domains
can be further organized into technical layering if so desired, which might take
the form app.customer.presentation, app.customer.business,
and so on. Notice that even though the customer domain logic may be organized
by technical usage, the primary structure (represented as the second node of the
namespace) is still partitioned by domain. Figure 2-4 shows a typical example of
a domain partitioned architecture.
Domain partitioned architectures have grown in popularity over the years in part
due to the increased use and acceptance of domain-driven design, a software
modeling and analysis technique coined by Eric Evans. Domain-driven design
places an emphasis on the design of a domain rather than on complex workflows
and technical components. This approach allows teams to collaborate closely
with domain experts and focus on one key part of the system, thus developing
software that closely resembles that domain functionality.
Figure 2-4. In a domain partitioned architecture, components are grouped by domain area
Description
Components within the layered architecture style are organized into horizontal
layers, each performing a specific role within the application (such as
presentation logic, business logic, persistence logic, and so on). Although the
number of layers may vary, most layered architectures consist of four standard
layers: presentation, business, persistence, and database (see Figure 3-1). In
some cases, the business layer and persistence layer are combined into a single
business layer, particularly when the persistence logic (such as SQL) is
embedded within the business layer components. Thus, smaller applications may
have only three layers, whereas larger and more complex business applications
may contain five or more layers.
Figure 3-1. The layered architecture style is a technically partitioned architecture
Each layer of the layered architecture style has a specific role and responsibility
within the application. For example, a presentation layer is responsible for
handling all user interface and browser communication logic, whereas a business
layer is responsible for executing specific business rules associated with the
request. Each layer in the architecture forms an abstraction around the work that
needs to be done to satisfy a particular business request. For example, the
presentation layer doesn’t need to know about how to get customer data; it only
needs to display that information on a screen in a particular format. Similarly, the
business layer doesn’t need to be concerned about how to format customer data
for display on a screen or even where the customer data is coming from; it only
needs to get the data from the persistence layer, perform business logic against
the data (e.g., calculate values or aggregate data), and pass that information up to
the presentation layer.
Layers are usually manifested through a namespace, package structure, or
directory structure (depending on the implementation language used). For
example, customer functionality in a business layer might be represented as
app.business.customer, whereas in the presentation layer, customer
logic would be represented as app.presentation.customer. In this
example, the second node of the namespace represents the layer, whereas the
third node represents the domain component. Notice, that the third node of the
namespace (customer) is duplicated for all of the layers—this is indicative of
a technically partitioned architecture, where the domain is spread across all
layers of the architecture.
One of the powerful features of the layered architecture style is the separation of
concerns among components. Components within a specific layer deal only with
logic that pertains to that layer. For example, components in the presentation
layer deal only with presentation logic, whereas components residing in the
business layer deal only with business logic. This type of component
classification makes it easy to build effective roles and responsibility models into
your architecture, and makes it easy to develop, test, govern, and maintain
applications using this architecture style when well-defined component
interfaces and contracts are used between layers.
Key Concepts
In this architecture style, layers can be either open or closed. Notice in Figure 3-
2 that each layer in the architecture is marked as being closed. A closed layer
means that as a request moves from layer to layer, it must go through the layer
right below it to get to the next layer below that one. For example, a request
originating from the presentation layer must first go through the business layer
and then to the persistence layer before finally hitting the database layer.
So why not allow the presentation layer direct access to either the persistence
layer or database layer? After all, direct database access from the presentation
layer is much faster than going through a bunch of unnecessary layers just to
retrieve or save database information. The answer to this question lies in a key
concept known as layers of isolation.
The layers of isolation concept means that changes made in one layer of the
architecture generally don’t impact or affect components in other layers: the
change is isolated to the components within that layer, and possibly another
associated layer (such as a persistence layer containing SQL). If you allow the
presentation layer direct access to the persistence layer, then changes made to
SQL within the persistence layer would impact both the business layer and the
presentation layer, thereby producing a very tightly coupled application with lots
of interdependencies between components. This type of architecture then
becomes brittle and very hard and expensive to change.
Figure 3-2. With closed layers, the request must pass through that layer
The layers of isolation concept also means that each layer is independent of the
other layers, thereby having little or no knowledge of the inner workings of other
layers in the architecture. To understand the power and importance of this
concept, consider a large refactoring effort to convert the presentation
framework from the angular.js framework to the react.js framework. Assuming
that the contracts (e.g., model) used between the presentation layer and the
business layer remain the same, the business layer is not affected by the
refactoring and remains completely independent of the type of user interface
framework used by the presentation layer. The same is true with the persistence
layer: if designed correctly, replacing a relational database with a NoSQL
database should only impact the persistence layer, not the presentation or
business layer.
While closed layers facilitate layers of isolation and therefore help isolate
change within the architecture, there are times when it makes sense for certain
layers to be open. For example, suppose you want to add a shared services layer
to an architecture containing common service functionality accessed by
components within the business layer (e.g., data and string utility classes or
auditing and logging classes). Creating a services layer is usually a good idea in
this case because architecturally it restricts access to the shared services to the
business layer (and not the presentation layer). Without a separate layer, there is
nothing that architecturally restricts the presentation layer from accessing these
common services, making it difficult to govern this access restriction.
In the shared services layer example, this layer would likely reside below the
business layer to indicate that components in this services layer are not
accessible from the presentation layer. However, this presents a problem in that
the business layer shouldn’t be required to go through the services layer to get to
the persistence layer. This is an age-old problem with the layered architecture,
and is solved by creating open layers within the architecture.
As illustrated in Figure 3-3, the services layer in this case should be marked as
open, meaning requests are allowed to bypass this layer and go directly to the
layer below it. In the following example, since the services layer is open, the
business layer is allowed to bypass it and go directly to the persistence layer,
which makes perfect sense.
Figure 3-3. With open layers, the request can bypass the layer below it
Leveraging the concept of open and closed layers helps define the relationship
between architecture layers and request flows, and provides designers and
developers with the necessary information to understand the various layer access
restrictions within the architecture. Failure to document or properly
communicate which layers in the architecture are open and closed (and why)
usually results in tightly coupled and brittle architectures that are very difficult to
test, maintain, and deploy.
Examples
To illustrate how the layered architecture works, consider a request from a
business user to retrieve customer information for a particular individual, as
illustrated in Figure 3-4. Notice the arrows show the request flowing down to the
database to retrieve the customer data, and the response flowing back up to the
screen to display the data.
Figure 3-4. An example of the layered architecture
In this example, the customer information consists of both customer data and
order data (orders placed by the customer). Here, the customer screen is
responsible for accepting the request and displaying the customer information. It
does not know where the data is, how it is retrieved, or how many database
tables must be queried to get the data.
Once the customer screen receives a request to get customer information for a
particular individual, it then forwards that request to the customer delegate
module in the presentation layer. This module is responsible for knowing which
modules in the business layer can process that request, and also how to get to
that module and what data it needs (the contract). The customer object in the
business layer is responsible for aggregating all of the information needed by the
business request (in this case to get customer information).
Next, the customer object module invokes the customer DAO (data access
object) module in the persistence layer to get customer data, and the order DAO
module to get order information. These modules in turn execute SQL statements
to retrieve the corresponding data and pass it back up to the customer object in
the business layer. Once the customer object receives the data, it aggregates the
data and passes that information back up to the customer delegate, which then
passes that data to the customer screen to be presented to the user.
Architecture Characteristics
The chart illustrated in Figure 3-5 summarizes the overall capabilities
(architecture characteristics) of the layered architecture in terms of star ratings.
One star means the architecture characteristic is not well supported, whereas five
stars means it’s well suited for that particular architecture characteristic.
Figure 3-5. Architecture characteristics star ratings for the layered architecture
Chapter 4. Microkernel
Architecture
Topology
The microkernel architecture style consists of two types of architecture
components: a core system and plug-in modules. Application logic is divided
between independent plug-in modules and the basic core system, providing
extensibility, flexibility, and isolation of application features and custom
processing logic. Figure 4-1 illustrates the basic topology of the microkernel
architecture style.
The core system of this architecture style can vary significantly in terms of the
functionality it provides. Traditionally, the core system contains only the
minimal functionality required to make the system operational (such as the case
with older IDEs such as Eclipse), but it can also be more full featured (such as
with web browsers like Chrome). In either case, the functionality in the core
system can then be extended through the use of separate plug-in modules.
Figure 4-1. Microkernel architecture style
Examples
A classic example of the microkernel architecture is the Eclipse IDE.
Downloading the basic Eclipse product provides you little more than a fancy
editor. However, once you start adding plug-ins, it becomes a highly
customizable and useful product for software development. Internet browsers are
another common example using the microkernel architecture: viewers and other
plug-ins add additional capabilities that are not otherwise found in the basic
browser (the core system). As a matter of fact, many of the developer and
deployment pipeline tools and products such as PMD, Jira, Jenkins, and so on
are implemented using microkernel architecture.
The examples are endless for product-based software, but what about the use of
the microkernel architecture for small and large business applications? The
microkernel architecture applies to these situations as well. Tax software,
electronics recycling, and even insurance applications can benefit from this
architecture style.
To illustrate this point, consider claims processing in a typical insurance
company (filing a claim for an accident, fire, natural disaster, and so on). This
software functionality is typically very complicated. Each jurisdiction (for
example, a state in the United States) has different rules and regulations for what
is and isn’t allowed in an insurance claim. For example, some jurisdictions allow
for a free windshield replacement if your windshield is damaged by a rock,
whereas other jurisdictions do not. This creates an almost infinite set of
conditions for a standard claims process.
Not surprisingly, most insurance claims applications leverage large and complex
rules engines to handle much of this complexity. However, these rules engines
can grow into a complex big ball of mud where changing one rule impacts other
rules, or requires an army of analysts, developers, and testers just to make a
simple rule change. Using the microkernel architecture style can mitigate many
of these issues.
For example, the stack of folders you see in Figure 4-2 represents the core
system for claims processing. It contains the basic business logic required by the
insurance company to process a claim (which doesn’t change often), but contains
no custom jurisdiction processing. Rather, plug-in modules contain the specific
rules for each jurisdiction. Here, the plug-in modules can be implemented using
custom source code or separate rules engine instances. Regardless of the
implementation, the key point is that jurisdiction-specific rules and processing
are separate from the core claims system and can be added, removed, and
changed with little or no effect on the rest of the core system or other plug-in
modules.
Architecture Characteristics
The chart illustrated in Figure 4-3 summarizes the overall capabilities
(architecture characteristics) of the microkernel architecture in terms of star
ratings. One star means the architecture characteristic is not well supported,
whereas five stars means it’s well suited for that particular architecture
characteristic.
Figure 4-3. Architecture characteristics star ratings for the microkernel architecture
Chapter 5. Event-Driven
Architecture
The event-driven architecture style has significantly gained in popularity and use
over recent years, so much so that even the way we think about it has changed.
This high adoption rate isn’t overly surprising given some of the hard problems
event-driven architecture solves, such as complex nondeterministic workflows
and highly reactive and responsive systems. Furthermore, new techniques, tools,
frameworks, and cloud-based services have made event-driven architecture more
accessible and feasible than ever before, and many teams are turning to event-
driven architecture to solve their complex business problems.
Topology
Event-driven architecture is an architecture style that relies on asynchronous
processing using highly decoupled event processors that trigger events and
correspondingly respond to events happening in the system. Most event-driven
architectures consist of the following architectural components: an event
processor, an initiative event, a processing event, and an event channel. These
components and their relationships are illustrated in Figure 5-1.
Figure 5-1. The main components of event-driven architecture
An event processor (today usually called a service) is the main deployment unit
in event-driven architecture. It can vary in granularity from a single-purpose
function (such as validating an order) to a large, complex process (such as
executing or settling a financial trade). Event processors can trigger
asynchronous events, and respond to asynchronous events being triggered. In
most cases, an event processor does both.
An initiating event usually comes from outside the main system and kicks off
some sort of asynchronous workflow or process. Examples of initiating events
are placing an order, buying some Apple stock, bidding on a particular item in an
auction, filing an insurance claim for an accident, and so on. In most cases,
initiating events are received by only one service that then starts the chain of
events to process the initiating event, but this doesn’t have to be the case. For
example, placing a bid on an item in an online auction (an initiating event) may
be picked up by a Bid Capture service as well as a Bid Tracker service.
A processing event (today usually referred to as a derived event) is generated
when the state of some service changes and that service advertises to the rest of
the system what that state change was. The relationship between an initiating
event and a processing event is one-to-many—a single initiating event typically
spawns many different internal processing events. For example, through the
course of a workflow, a Place Order initiating event may result in an Order
Placed processing event, a Payment Applied processing event, a
Inventory Updated processing event, and so on. Notice how an initiating
event is usually in noun-verb format, whereas a processing event is usually in
verb-noun format.
The event channel is the physical messaging artifact (such as a queue or topic)
that is used to store triggered events and deliver those triggered events to a
service that responds to those events. In most cases initiating events use a point-
to-point channel using queues or messaging services, whereas processing events
generally use publish-and subscribe channels using topics or notification
services.
Example Architecture
To see how all of these components work together in a complete event-driven
architecture, consider the example illustrated in Figure 5-2 where a customer
wants to order a copy of Fundamentals of Software Architecture by Mark
Richards and Neal Ford (O’Reilly). In this case, the initiating event would be
Place Order. This initiating event is received by the Order Placement
service, which then places the order for the book. The Order Placement
service in turn advertises what it did to the rest of the system through a Order
Placed processing event.
Notice in this example that when the Order Placement service triggers the
Order Placed event, it doesn’t know which other services (if any) respond to
this event. This illustrates the highly decoupled, nondeterministic nature of
event-driven architecture.
Continuing with the example, notice in Figure 5-2 that three different services
respond to the Order Placed event: the Payment service, the Inventory
Management service, and the Notification service. These services
perform their corresponding business functions, and in turn advertise what they
did to the rest of the system through other processing events.
Figure 5-2. Processing a book order using event-driven architecture
One thing in particular to notice about this example is how the Notification
service advertises what it did by generating a Notified Customer
processing event, but no other service cares about or responds to this event. So
why then trigger an event that no one cares about? The answer is architectural
extensibility. By triggering an event, the Notification service provides a
hook that future services can respond to (such as a notification tracking service),
without having to make any other modifications to the system. Thus, a good rule
of thumb with event-driven architecture is to always have services advertise their
state changes (what action they took), regardless if other services respond to that
event. If no other services care about the event, then the event simply disappears
from the topic (or is saved for future processing, depending on the messaging
technology used).
Figure 5-3. With events, the sender owns the event channel and contract
The type of event channel artifact is also a distinguishing factor between event-
driven systems and message-driven systems. Typically, event-driven systems use
publish-and-subscribe messaging using topics or notification services when
triggering events, whereas message-driven systems typically use point-to-point
messaging using queues or messaging services when sending messages. That’s
not to say event-driven systems can’t use point-to-point messaging—in some
cases point-to-point messaging is necessary to retrieve specific information from
another service or to control the order or timing of events in a system.
Architecture Characteristics
The chart illustrated in Figure 5-5 summarizes the overall capabilities
(architecture characteristics) of event-driven architecture in terms of star ratings.
One star means the architecture characteristic is not well supported, whereas five
stars means it’s well suited for that particular architecture characteristic.
Figure 5-5. Architecture characteristics star ratings for event-driven architecture
Chapter 6. Microservices
Architecture
Basic Topology
The microservices architecture style is an ecosystem made up of single-purpose,
separately deployed services that are accessed typically through an API gateway.
Client requests originating from either a user interface (usually a microfrontend)
or an external request invoke well-defined endpoints in an API gateway, which
then forwards the user request to separately deployed services. Each service in
turn accesses its own data, or makes requests to other services to access data the
service doesn’t own. The basic topology for the microservices architecture style
is illustrated in Figure 6-1.
Figure 6-1. The basic topology of the microservices architecture style
Notice that although Figure 6-1 shows each service associated with a separate
database, this does not have to be the case (and usually isn’t). Rather, each
service owns its own collection of tables, usually in the form of a schema that
can be housed in a single highly available database or a single database devoted
to a particular domain. The key concept to understand here is that only the
service owning the tables can access and update that data. If other services need
access to that data, they must ask the owning microservice for that information
rather than accessing the tables directly. The reasoning behind this data
ownership approach is described in detail in “Bounded Context”.
The primary job of the API gateway in microservices is to hide the location and
implementation of the corresponding services that correspond to the API
gateway endpoints. However, the API gateway can also perform cross-cutting
infrastructure-related functions, such as security, metrics gathering, request-ID
generation, and so on. Notice that unlike the enterprise service bus (ESB) in
service-oriented architecture, the API gateway in microservices does not contain
any business logic, nor does it perform any orchestration or mediation. This is
critical within microservices in order to preserve what is known as a bounded
context (detailed further in a moment).
What Is a Microservice?
A microservice is defined as a single-purpose, separately deployed unit of
software that does one thing really, really well. In fact, this is where the term
“microservices” gets its name—not from the physical size of the service (such as
the number of classes), but rather from what it does. Because microservices are
meant to represent single-purpose functions, they are generally fine-grained.
However, this doesn’t always have to be the case. Suppose a developer creates a
service consisting of 312 class files. Would you still consider that service to be a
microservice? In this example, the service actually does only one thing really
well—send emails to customers. Each of the 300+ different emails that could be
sent to a customer is represented as a separate class file, hence the large number
of classes. However, because it does one thing really well (send an email to a
customer), this would in fact be consisted a microservice. This example
illustrates the point that its not about the size of the service, but rather what the
service does.
Because microservices tend to be single-purpose functions, it’s not uncommon to
have hundreds to even thousands of separately deployed microservices in any
given ecosystem or application context. The sheer number of separate services is
what makes microservices so unique. Microservices can be deployed as
containerized services (such as Docker) or as serverless functions.
Bounded Context
As mentioned earlier, each service typically owns its own data, meaning that the
tables belonging to a particular service are only accessed by that service. For
example, a Wishlist service might own its corresponding wishlist tables.
If other services need wish list data, those services would have to ask the
Wishlist service for that information rather than accessing the wishlist
tables directly.
This concept is known as a bounded context, a term coined by Eric Evans in his
book Domain-Driven Design (Addison-Wesley). Within the scope of
microservices, this means that all of the source code representing some domain
or subdomain (such as a wish list for a customer), along with the corresponding
data structures and data, are all encapsulated as one unit, as illustrated in
Figure 6-2.
Figure 6-2. A bounded context includes the source code and corresponding data for a given domain or
subdomain
Unique Features
Microservices stands apart from all other architecture styles. The three things
that make the microservices architecture style so unique are distributed data,
operational automation, and organizational change.
Microservices is the only architecture style that requires data to be broken up
and distributed across separate services. The reason for this need is the sheer
number of services usually found within a typical microservices architecture.
Without aligning services with their corresponding data within a strict bounded
context, it simply wouldn’t be feasible to make structural changes to the
underlying application data. Because other architecture styles don’t specify the
fine-grained, single-purpose nature of a service as microservices does, those
other architecture styles can usually get by with a single monolithic database.
Although the practice of associating a service with its corresponding data in a
bounded context is one of the main goals of microservices, rarely in the real
world of business applications does this completely happen. While a majority of
services may be able to own their own data, in many cases it’s sometimes
necessary to share data between two or more services. Use cases for sharing data
between a handful of services (two to six) range from table coupling, foreign key
constraints, triggers between tables, and materialized views, to performance
optimizations for data access, to shared ownership of tables between services.
When data is shared between services, the bounded context is extended to
include all of the shared tables as well as all of the services that access that data.
Operational automation is another unique feature that separates microservices
from all other architecture styles, again due to the sheer number of microservices
in a typical ecosystem. It is not humanly possible to manage the parallel testing,
deployment, and monitoring of several hundred to several thousand separately
deployed units of software. For this reason, containerization is usually required,
along with service orchestration and management platforms such as Kubernetes.
This also leads to the requirement of DevOps for microservices (rather than
something that’s “nice to have”). Because of the large number of services, it’s
not feasible to “hand off” services to separate testing teams and release
engineers. Rather, teams own services and the corresponding testing and release
of those services.
This leads to the third thing that distinguishes microservices from all other
architecture styles—organizational change. Microservices is the only
architecture style that requires development teams to be organized into domain
areas of cross-functional teams with specialization (a single development team
consisting of user interface, backend, and database developers). This in turn
requires the identification of service owners (usually architects) within a
particular domain. Testers and release engineers, as well as DBAs (database
administrators), are also usually aligned with specific domain areas so that they
are part of the same virtual team as the developers. In this manner, these “virtual
teams” test and release their own services.
Most web-based business applications follow the same general request flow: a
request from a web browser is received by a web server, then an application
server, then finally a database server. While this type of request flow works great
for a small number of users, bottlenecks start appearing as the user load
increases, first at the web server, then at the application server, and finally at the
database.
The usual response to bottlenecks based on an increase in user load is to scale
out the web servers. This is relatively easy and inexpensive, and sometimes
works to address some bottleneck issues. However, in most cases of high user
load, scaling out the web servers just moves the bottleneck down to the
application servers. Scaling application servers can be more complex and
expensive than web servers, and usually just moves the bottleneck down to the
database, which is even more difficult and expensive to scale. Even if you can
scale the database, what you eventually end up with is a triangle-shaped
topology shown in Figure 7-1, with the widest part of the triangle being the web
servers (easiest to scale) and the smallest part being the database (hardest to
scale).
Figure 7-1. The database is usually the ultimate bottleneck for highly scalable systems
In any high-volume application with an extremely large concurrent user load, the
database will usually be the final limiting factor in how many transactions you
can process concurrently. While various caching technologies and database
scaling and sharding products help to address these issues, the fact remains that
scaling out an application for extreme loads is a very difficult proposition when
it comes to the database.
The space-based architecture style is specifically designed to address and solve
these sorts of high scalability and concurrency issues. It is also a useful
architecture style for applications that have variable and unpredictable
concurrent user volumes (known as elastic systems). Solving extreme and
variable scalability needs is exactly what space-based architecture is all about.
Figure 7-3. The processing unit contains the application functionality and an in-memory data grid
The complexity associated with this architecture style is managed through what
is called virtualized middleware. This middleware manages such things as
request and session management, data synchronization, communication and
orchestration between processing units, and the dynamic tearing down and
starting up of processing units to manage elasticity and user load. The four main
architecture components contained in the virtualized middleware are the
messaging grid, the data grid, the processing grid, and the deployment manager.
The messaging grid manages input request and session information. When a
request comes into the virtualized middleware component, the messaging grid
component determines which active processing units are available to receive the
request and forwards the request to one of them. The complexity of the
messaging grid can range from a simple round-robin algorithm to a more
complex next-available algorithm that keeps track of which request is being
processed by which processing unit. Typically, the messaging grid is
implemented through a traditional web server.
The data grid component is perhaps the most important and crucial component
in this style. The data grid interacts with the data-replication engine in each
processing unit to manage the data replication between processing units when
data updates occur. Since the messaging grid can forward a request to any of the
processing units available, it is essential that each processing unit contains
exactly the same data in its in-memory data grid as other processing units. The
data grid is typically implemented through caching products such as Hazelcast,
Apache Ignite, and Oracle Coherence, which manage the synchronization and
replication of the data grids. This synchronization typically occurs
asynchronously behind the scenes as updates occur in the in-memory data grids.
An additional element of the data grid is a data pump that asynchronously sends
the updates to a database. A data pump can be implemented in a number of
ways, but is typically managed through persistent queues using messaging or
streaming. Components called data writers asynchronously listen for these
updates, and in turn, update the database. Data writers can be implemented in a
number of ways, varying in granularity from application-level custom data
writers or data hubs to dedicated data writers for each processing unit type.
In the event of a cold start due to a system crash or a deployment, data readers
are used, leveraging a reverse data pump to retrieve data from the database and
pump the data to a processing unit. However, once at least one processing unit
having the same in-memory data grid is populated, additional processing units
can be started and populated without having to retrieve the data from the
database. Figure 7-4 illustrates the data grid containing the in-memory data
grids, data pumps, data writers, and data readers.
The processing grid component of the virtualized middleware is an optional
component that manages distributed processing for requests that require
coordination or orchestration between processing unit types. Orchestration
between multiple processing units can be coordinated through the processing
grid, or directly between processing units in a choreographed fashion.
Figure 7-4. The data grid contains the in-memory data grid, data pumps, and data writers
Finally, the deployment manager component manages the dynamic startup and
shutdown of processing units based on load conditions. This component
continually monitors response times and user loads, starts up new processing
units when the load increases, and shuts down processing units when the load
decreases. It is a critical component to achieving variable scalability needs
within an application, and is usually implemented through container
orchestration products such as Kubernetes.
Examples
Space-based architecture is a very complicated and specialized architecture style,
and is primarily used for high-volume, highly elastic systems that require very
fast performance.
One example of the use of space-based architecture is a concert ticketing system.
Imagine what happens when your favorite rock band announces an opening
show and tickets go on sale. Concurrency goes from a few dozen people to tens
of thousands of people within a matter of seconds, with everyone wanting those
same great seats you want. Continuously reading and writing to a database
simply isn’t feasible with this kind of elastic system at such a high scale and
performance.
Another example of the kind of elastic systems that benefit from space-based
architecture is online auction and bidding systems. In most cases, sellers have no
idea how many people will be bidding, and bidding always gets fast and furious
toward the end of the bidding process, significantly increasing the number of
concurrent requests. Once the bidding ends, requests go back down to a
minimum, and the whole process repeats again as bidding nears the end—
another good example of an elastic system.
High-volume social media sites are another good example where space-based
architecture is a good fit. How do you process hundreds of thousands (or even
millions) of posts, likes, dislikes, and responses within a span of a few seconds?
Clearly the database gets in the way of this sort of volume (regardless of elastic
behavior), and removing the database from the transactional processing, as
space-based architecture does, and eventually persisting the data is one possible
solution to this complex problem.
Architecture Characteristics
The chart illustrated in Figure 7-5 summarizes the overall capabilities
(architecture characteristics) of space-based architecture in terms of star ratings.
One star means the architecture characteristic is not well supported, whereas five
stars means it’s well suited for that particular architecture characteristic.
Figure 7-5. Architecture characteristics star ratings for space-based architecture
Appendix A. Style Analysis
Summary
Figure A-1 summarizes the architecture characteristics scoring for each of the
architecture styles described in this report. One dot means that the architecture
characteristic is not well supported by the architecture style, whereas five dots
means it’s well supported by that style.
This summary will help you determine which style might be best for your
situation. For example, if your primary architectural concern is scalability, you
can look across this chart and see that the event-driven style, microservices style,
and space-based style are probably good architecture style choices. Similarly, if
you choose the layered architecture style for your application, you can refer to
the chart to see that deployment, performance, and scalability might be risk areas
in your architecture.
While this chart and report will help guide you in choosing the right style, there
is much more to consider when choosing an architecture style. You must analyze
all aspects of your environment, including infrastructure support, developer skill
set, project budget, project deadlines, and application size, to name a few.
Choosing the right architecture style is critical, because once an architecture is in
place, it is very hard (and expensive) to change.
Figure A-1. Architecture styles rating summary
About the Author
Mark Richards is an experienced, hands-on software architect involved in the
architecture, design, and implementation of microservices architectures, service-
oriented architectures, and distributed systems in a variety of technologies. He
has been in the software industry since 1983 and has significant experience and
expertise in application, integration, and enterprise architecture. Mark is the
founder of DeveloperToArchitect.com, a free website devoted to helping
developers in the journey to becoming a software architect.
In addition to hands-on consulting and training, Mark has authored numerous
technical books and videos, including the two latest books he coauthored,
Fundamentals of Software Architecture (O’Reilly) and Software Architecture:
The Hard Parts (O’Reilly). Mark has spoken at hundreds of conferences and
user groups around the world on a variety of enterprise-related technical topics.
When he is not working, Mark can usually be found hiking in the White
Mountains or along the Appalachian Trail.