Smart endpoints and dumb pipes
This page is
published under the terms of the licence summarized in the footnote.
In a monolithic application, the
various components are just a procedure call away.
But in a distributed software architecture,
components are distributed across (virtual or real) machines.
So, how to
integrate application components in a distributed software architecture?
Martin Fowler’s 2014 paper defining
microservices recommends
integrating application components using principle of “smart endpoints and dumb
pipes”.
The example
below is edited from an article by Nathan Peck (Sep 1 2017) that elaborates on
Fowler’s concept, and explains modern techniques for integrating application
components.
https://medium.com/@nathankpeck/microservice-principles-smart-endpoints-and-dumb-pipes-5691d410700f
For
compatibility with TOGAF and Avancier Methods I’ve replaced almost all microservice
by application component.
Contents
Integration
patterns - an example
The
use case: sign up to create an account
An
anti-pattern: Hub and Spoke
The two
primary forms of communication between application components are here called Request-Reply
and Observer.
Synchronous
request-reply
A client component invokes a server component by making an explicit request, usually to store or retrieve data.
The client component waits for a response, be it a resource or a simply an acknowledgement.
Asynchronous observer
One component
publishes an event.
One or more
observers (watching for that event) respond by running logic asynchronously,
outside the awareness of the event producer.
For some, loose-coupling via messaging has become a mantra, but there are always trade offs, as this table shows.
Couple
via synchronous request/reply for |
Decouple via asynchronous messages/events for |
Speed Simplicity Consistency |
Scale (very high volume and/or throughput) Adaptability/Manageability (of smaller distributed
components) Availability (of
smaller units of work) and Partition
Tolerance |
Martin Fowler points out that many put significant intelligence into the integration mechanisms.
An Enterprise Service Bus (ESB) includes sophisticated facilities for message routing, choreography, transformation, and applying business rules.
Components are integrated using complex protocols such as WS-Choreography or BPEL.
By contrast “smart endpoints and dumb pipes” means integrating application components that own their own domain logic.
And components are integrated (choreographed or orchestrated) using simple protocols and/or lightweight messaging.
The simple protocols are ones the world wide web (and to a large extent, Unix) is built on.
Typlically, messages are sent using HTTP to remote components/resources that are identified using domain names.
Oft-used resources can be cached locally (with little effort).
The lightweight messaging technology is dumb - acts as a message router only.
Simple messaging products (as RabbitMQ or ZeroMQ) do little more than provide reliable message queues.
The term micro implies subdividing something that could have been macro.
As always, there are trade offs.
Component size |
Process length |
Message quantity |
|
Macro pattern |
larger components
perform |
longer processes |
with less
inter-component messaging |
Micro pattern |
smaller
components perform |
shorter processes |
with more
inter-component messaging |
Distributing components tends to mean designers have to consider two things
Reducing message
frequency
Local procedure calls are simple, fast, reliable and cheap.
Designers don’t hesitate to design frequent communication between fine-grained operations.
Remote procedure calls and messaging technologies are more complex, slower, less reliable and more costly,
Designers need to consider making communication less frequent communication and operations coarser -grained.
Compensating
transactions for abortive business processes
Asynchronous integration tends to mean accepting user commands and embarking on business processes that later have to be abandoned.
Contrary to design for simplicity, designers need to consider compensating transactions.
This issue is explored in our paper on CQRS.
Understanding where and when to use a
Request-Reply model versus an observer model is a key to designing effective
application component integration.
To illustrate these two types of
communication consider the following software architecture for a basic social
media application.
The user enters their basic profile
details such as name, password, email address, etc.
The users’s client device makes a synchronous
(?) POST request to an API endpoint
Request-reply pattern
The API endpoint sends the request on
to two backend components.
·
a
core user metadata component that stores the basic profile information,
·
a
password component that hashes the password plaintext and stores it for later
comparison when the user logs in.
These two initial requests are
explicit Request-Reply communications.
The user signup endpoint can’t return
a “200 OK” response to the client until both of these pieces of data have been
persisted in the backend.
Observer pattern
By contrast, there are two more
application components that run in the background to process the user sign up..
·
one
component emails the user asking them to click a link to verify their email
address.
·
another
component starts searching for friends to recommend to the user.
The client shouldn’t have to wait for
a verification email to be sent out, or friend recommendations to be generated.
For these two features it is better to
have an observer pattern where the backend components are watching for the user
signup event and are triggered asynchronously.
So, the API endpoint also publishes an
event that backend application components can process asynchronously.
User signup for a social media
application |
||||
Client component |
Data flow |
API end point |
Data flow |
Server components |
Mobile device |
POST |
User Signup |
Request-Reply |
Profile |
Request-Reply |
Password |
|||
User Signup Event |
Email Verification |
|||
Friend Recommendation |
Diagram version
Building a complex centralized
communication bus that runs logic to route and manipulate messages is an architectural
pitfall that tends to cause problems later on.
Instead, microservices favor a
decentralized approach: components use dumb pipes to get messages from one
endpoint to another endpoint.
At first glance, a hub and spoke
diagram with a central bus looks less scary than a network of many direct
point-to-point connections.
However, the tangle of connections
still exists in the central bus - embedded in one monolithic component.
This central monolith has a tendency
to become excessively complex and can become a bottleneck both from a
performance standpoint as well as an engineering standpoint.
Decentralized application component
integrations enable teams to work in parallel on different edges of the
architecture without breaking other components.
Each application component treats
other components as external resources with no differentiation between internal
components and third party resources.
This means each application component
encapsulates its own logic for formatting its outgoing responses or
supplementing its incoming requests.
This allows teams to independently add
features or modify existing features without needing to modify a central bus.
Scaling the application component
integrations is also decentralized so that each component can have its own load
balancer and scaling logic.
Request-Reply communication is used
anywhere one component sends a request and expects either a resource or
acknowledgment response to be returned.
The most basic way to implement this
pattern is using HTTP, ideally following REST principles.
A standard HTTP based communication
pipeline between two application components typically looks like this:
Client Component |
HTTP Load Balancer |
Server component 1 |
Server component 1 |
||
Server component 3 |
Diagram version
In this approach, a simple load
balancer can sit in the middle.
The originating component can make an
HTTP request to a load balancer.
The load balancer can forward the
request to one of the instances of the backing application component.
However, in some cases there is
extremely high traffic between components, or teams want to reduce latency
between application components as much as possible.
In this case they may adopt thick
client load balancing.
This approach typically uses a system
such as Consul, Eureka, or ZooKeeper to keep track of the set of
application component instances and their IP addresses.
Then the originating application
component is able to make a request directly to an instance of the backing
component that it needs to talk to.
Client Component |
DNS Record user.service.dcl.consul |
Server component 1 user-01.service.dcl.consul |
Server component 1 user-01.service.dcl.consul |
||
Server component 3 user-01.service.dcl.consul |
Diagram version
Consul maintains a DNS record that
resolves to the different backend application component instances.
So that one component can talk
directly to another component, with no load balancer in-between.
Another framework is GRPC, which has emerged as a strong
convention for polyglot applications.
GRPC can operate using an external
load balancer, similar to the HTTP approach above, or it can also use a thick
client load balancer.
The distinguishing characteristic of
GRPC, however, is that it translates communication payloads into a common
format called protocol buffers.
Diagram version
Protocol buffers allow backend
components to serialize communication payloads into an efficient binary format
for travel over the wire.
But then deserialize it into the
appropriate object model for their specific runtime.
Observer communication is critical to
scaling out components.
Not every communication requires a
response or an acknowledgement.
In fact, in many workflows there are
at least a few pieces of logic that should be fully asynchronous and
non-blocking.
The standard way for distributing this
type of workload is to pass messages using a broker component, ideally one that
implements a queue.
RabbitMQ, ZeroMQ, Kafka, or even Redis
Pub/Sub can all be used as dumb pipes that allow an application component to
publish an event,
while allowing other application
components to subscribe to one or more classes of events that they need to
respond to.
Diagram version
Organizations running workloads on
Amazon Web Services often favor using Amazon
Simple Notification Service (SNS),
and Amazon
Simple Queue Service (SQS) as a fully managed solution for broadcast communication.
These components allow a producing
component to make one request to an SNS topic to broadcast an event.
While multiple SQS queues can be
subscribed to that topic, with each queue connected to a single application
component that consumes and responds to that event.
Decoupling
The huge advantage of this approach is
that a producing component need not how many subscribers there are to the
event, or what they are doing in response to the event.
In the event of a consumer failure
most queuing systems have a retry / redelivery feature to ensure that the
message is eventually processed.
The producer can just “fire and
forget”, trusting that the message broker’s queue will ensure that the message
eventually reaches the right consumers.
Even if all consumers are too busy to
respond to the event immediately, the queue will hold onto the event until a
consumer is ready to process it.
Extendability
Another benefit of the observer model is
future extendability of a system.
Once an event broadcast is implemented
in a producer component, new types of consumers can be added and subscribed to
the event after the fact without needing to change the producer.
For example, in the social media application
at the start of the article there were two consumers subscribed to the user
signup event (for email verification, and friend recommendation).
Engineers could easily add a third
component that responded to the user signup event by emailing all the people
who have that new user in their contacts to let them know that a contact signed
up.
This would require no changes to the
user signup component, eliminating the risk that this new feature might break
the critical user signup feature.
The observer model is a very powerful
tool, and no software architecture can reach its full potential without having
at least some observer-based communications.
The principle of smart endpoints and
dumb pipes is easy to understand when you embrace the concept of
decentralization of architecture and logic.
Despite using “dumb pipes” application
components can still implement essential messaging primitives without the need
for a centralized Message Bus.
Instead, application components should
make use of the broad ecosystem of frameworks that exist as dumb pipes for both
Request-Reply and observer communications.
Footnote: Creative Commons Attribution-No Derivative Works Licence
2.0 10/01/2019 12:44
Attribution: You may copy, distribute and display this copyrighted work
only if you clearly credit “Avancier Limited: http://avancier.website” before the start and include this footnote at the end.
No Derivative Works: You may copy, distribute, display
only complete and verbatim copies of this page, not derivative works based
upon it.
For more information about the licence, see http://creativecommons.org