Patterns for Solving Problems
in Serverless Architectures
From Serverless Architectures on
AWS by Peter Sbarski
In this article, you will learn about useful
patterns for solving design problems in
serverless architectures.
Save 37% on Serverless Architectures on AWS. Just enter code fccsbarski into the discount
code box at checkout at [Link].
Patterns
Patterns are architectural solutions to problems in software design. They’re designed to
address common problems found in software development. They’re also an excellent
communication tool for developers working together on a solution. It’s far easier to find an
answer to a problem if everyone in the room understands which patterns are applicable, how
they work, and their advantages and disadvantages. The patterns presented in this article are
useful for solving design problems in serverless architectures. These patterns aren’t exclusive
to serverless. In fact, they’ve were used in distributed systems long before serverless
technologies became viable. Apart from the patterns presented in this article we recommend
that you become familiar with patterns relating to authentication, data management (CQRS,
Event Sourcing, Materialized Views, Sharding), and error handling (Retry Pattern). Learning
and applying these patterns will make you a better software engineer, regardless of the
platform you use.
Command pattern
Figure 1. The command pattern is used to invoke and control functions and services from a
single function.
A single end-point can be used to cater to different requests with different data because it can
accept any combination of fields from a client and create a response that matches the request.
The same idea can be applied more generally. We can design a system in which a specific
Lambda function controls and invokes other functions. You can connect it to an API Gateway
or invoke it manually, and pass messages to it to invoke other Lambda functions.
In software engineering, the command pattern is used to “encapsulate a request as an object,
thereby letting you parameterize clients with different requests, queue or log requests, and
support undoable operations” because of the “need to issue requests to objects without
knowing anything about the operation being requested or the receiver of the request”
([Link] The command pattern allows us to decouple the caller of the
operation from the entity that carries out the required processing.
In practice, this pattern can simplify the API Gateway implementation (as you may not want
or need to create a RESTful URI for every type of request). It can also make versioning
simpler. The Command Lambda function could work with different versions of your clients
and invoke the right Lambda function needed by the client.
When to use this
This pattern is useful if you want to decouple the caller and the receiver. Having a way to
pass arguments as an object, and allowing “clients to be parametrized with different
requests,” can reduce coupling between components and help make the system more
extensible. Be aware of using this approach if you need to return a response to the API
Gateway. Adding another function will increase latency.
Messaging pattern
Figure 2. The messaging pattern, and its many variations, are popular in distributed
environments.
Messaging patterns are popular in distributed systems because they allows developers to
build scalable and robust systems by decoupling functions and services from direct
dependence on one another, and allowing storage of events/records/requests in a queue. The
reliability comes from the fact that if the consuming service goes offline, messages are
retained in the queue and can still be processed at a later time.
This pattern features a message queue with a sender that can post to the queue and a receiver
that can retrieve messages from the queue. In terms of implementation in AWS, we can build
this pattern on top of the Simple Queuing Service (SQS). Unfortunately, Lambda doesn’t
integrate directly with SQS, and one approach to addressing this problem is to run a Lambda
function on a schedule and let it check the queue occasionally.
Depending on how the system is designed, a message queue can have a single sender/receiver
or multiple senders/receivers. SQS queues typically have one receiver per queue. If you
needed to have multiple consumers, a straightforward way to do it is to introduce multiple
queues in to the system (figure 3). A strategy you could apply is to combine SQS and
Amazon Simple Notification Service (SNS) together. SQS queues could subscribe to an SNS
topic; pushing a message to the topic automatically pushes the message to the subscribed
queues.
Kinesis Streams is an alternative to SQS, although it doesn’t have some features, such as
dead lettering of messages ([Link] Kinesis Streams integrates with
Lambda, provides an ordered sequence of records, and supports multiple consumers.
Figure 3. Your system may have multiple queues/streams and Lambda functions to process
all incoming data.
When to use this
This is a popular pattern used to handle workloads and data processing. The queue serves as a
buffer, and if the consuming service crashes, data isn’t lost. It remains in the queue until the
service can restart and begin processing it again. A message queue can make future changes
easier because there’s less coupling between functions. In an environment with a lot of data
processing, messages, and requests, try to minimize the number of functions that are directly
dependent on other functions, and use the messaging pattern instead.
Priority queue pattern
Figure 4. The priority queue pattern is an evolution of the messaging pattern.
A great benefit of using a platform such as AWS and serverless architectures is that capacity
planning and scalability is more of a concern for Amazon’s engineers than for us. In some
cases, we may want to control how and when messages get dealt with by your system. This is
where you might need to have different queues, topics, or streams to feed messages to your
functions. Your system might go one step further and have entirely different workflows for
messages of different priority. Messages that need immediate attention might go through a
flow that expedites the process by using more expensive services and APIs with more
capacity. Messages that don’t need to be processed quickly can go through a different
workflow.
This pattern might involve creation and use of entirely different SNS topics, Kinesis streams,
SQS queues, Lambda functions, and even third-party services. Try to use this pattern
sparingly, because additional components, dependencies, and workflows results in more
complexity.
When to use this
This pattern works when you need to have a different priority on processing of messages.
Your system can implement workflows and use different services and APIs to cater for many
types of needs and users (for example, paying versus non-paying users).
Fan-out pattern
Figure 5. The fan-out pattern is useful, as many AWS services (such as S3) can’t invoke
more than one Lambda function when an event takes place.
Fan-out is a type of messaging pattern familiar to many users of AWS. Generally, the fan-out
pattern is used to push a message out to all listening/subscribed clients of a particular queue
or a message pipeline. In AWS, this pattern is usually implemented using SNS topics that
allow multiple subscribers to be invoked when a new message is added to a topic. Take S3 as
an example. When a new file is added to a bucket, S3 can invoke a single Lambda function
with information about the file. What if you need to invoke two, three, or more Lambda
functions at the same time? The original function could be modified to invoke other functions
(like the Command pattern) but it’s a lot of work if all you need is to run functions in parallel.
The answer is to use the fan-out pattern using SNS.
SNS topics are communications/messaging channels that can have multiple publishers and
subscribers (including Lambda functions). When a new message is added to a topic, it forces
invocation of all subscribers in parallel, causing the event to fan out. Going back to the S3
example, instead of invoking a single message Lambda function, you can configure S3 to
push a message on to an SNS topic to invoke all subscribed functions at the same time. It’s an
effective way to create event-driven architectures and perform operations in parallel.
When to use this
This pattern is useful if you need to invoke multiple Lambda functions at the same time. An
SNS topic tries and retries to invoke your Lambda functions if it fails to deliver the message
or if the function fails to execute. Furthermore, the fan-out pattern can be used for more than
invocation of multiple Lambda functions. SNS topics support other subscribers, such as email
and SQS queues too. Adding a new message to a topic can invoke Lambda functions, send an
email, or push a message on to an SQS queue, all at the same time.
Pipes and filters pattern
Figure 6. This pattern encourages the construction of pipelines to pass and transform data
from its origin (pump) to its destination (sink).
The purpose of the pipes and filters pattern is to decompose a complex processing task to a
series of manageable, discrete services organized in a pipeline (figure 6). Components
designed to transform data are traditionally referred to as a filters, whereas connectors that
pass data from one component to the next component are referred to as a pipe. Serverless
architecture lends itself well to this kind of pattern. These are useful for all kinds of tasks
where multiple steps need to be taken to achieve a result.
We recommend that every Lambda function be written as a granular service or a task with the
Single Responsibility Principle (SRP) in mind. Inputs and outputs should be clearly defined
to create a clear interface, and minimize side effects. Following this advice allows you to
create functions that can be reused in pipelines and more broadly within your serverless
system. The compute as glue architecture is closely inspired by this pattern.
When to use this
Whenever you have a complex task, try to break it down into a series of functions (a pipeline)
and apply the following rules:
Make sure your function follows the Single Responsibility Principle.
Make the function idempotent (your function should always produce the same output for
given input).
Clearly define an interface for the function. Make sure that inputs and outputs are clearly
stated.
Create a black box. The consumer of the function shouldn’t have to know how it works
but must know to use it and what kind of output to expect every time.
That’s all for this article.
For more, check out the whole book on liveBook h