|From Serverless Architectures on AWS by Peter Sbarski
In this article, you will learn about useful patterns for solving design problems in serverless architectures.
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.
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” (http://bit.ly/29ZaoWt). 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.
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 (http://amzn.to/2a3HJzH). 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).
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 here.