Serverless Guide - Structure of a Serverless Application

May 12, 2022

Introduction

When building a Serverless application, one aspect that should be focused on is how the individual services that make up your application are structured. Typically, a multi-service architecture is recommended when building Serverless applications and this guide will provide more answers to questions you may have when it comes to structuring them effectively.

Service Size

Starting out

When starting out, keeping all functionality within a single monolithic stack often provides more advantages. The size of a single Serverless Framework service can be guided by a number of factors. However, when getting started building a Serverless application for the first time, especially if this is potentially a limited set of functionality or the serverless development is being trialled in a proof of concept, a single service for the entire application is advised.

It is in the early stages where there are the most questions and unknowns about the resulting application to be built, and so trying to, at this early stage, break apart the feature set of an application into discrete domains may be difficult to do. There is also the benefit that having all code and configuration in a single service makes it a lot easier for all members of the team to very quickly and easily be onboarded and have a complete overview of what is happening within the application.

It is also possible that a single service may be all that is ever needed. Some applications will never reach a point where breaking a service apart into many smaller pieces makes sense. And if you ever do get to that point, it is far easier to break a larger service up into smaller pieces as needed than attempting to merge them together later.

As you grow

One of the advantages that serverless development provides is that it is a lot easier than most other methodologies to split large services into smaller ones. As your Serverless application grows and matures, determining service boundaries becomes a lot easier to do as well. And being Serverless, creating a new service based on functionality that already exists within another service with its own serverless.yml is relatively quick to do.

The reason you may need to consider breaking up a single large Serverless service into multiple is for a number of reasons:

  1. There exist certain limits within AWS that can restrict your ability to keep making services larger, such as the CloudFormation stack size limit, the IAM permissions document size limit and others.
  2. Certain AWS services provide capacity per resource and by splitting services, you end up creating multiple of those resources; such as API Gateway
  3. Certain region limits may come into play such as the limit on the number of concurrent Lambda functions and having an application split across multiple services means you could move some services to other AWS regions to benefit from additional capacity.
  4. If the team grows large enough, splitting services up allows for better organisation, especially as the application requires multiple teams to develop and maintain it.

When deciding on service size and where domain boundaries should lie, not going all in on microservices is often a good choice. You do not necessarily need to become so fine grained in your domain boundaries that you are considered microservices, but just large enough so that the domain is cohesive and sensibly bound. What sensibly and cohesive means to you will differ by organisation, team and the application itself.

While microservices provide essentially all the same benefits as a collection of services with larger domains, there is one very large downside to going the fully microservices path; and that is complexity. Distributed applications are hard to build and manage over time and the greater in size and smaller the collection of services are the higher the complexity becomes. Serverless development should be focussed on simplicity and keeping the complexity of the application to a minimum.

Inter-service Communication

If you are creating multiple Serverless services, you often need to find ways to allow for communication between the various services. While we will look at each in depth, but generally speaking the order of preference for communication methods between services is:

  1. Asynchronous: This method allows a service to submit a message of some type to a managed service that is eventually read and processed by a downstream service.
  2. Synchronous: A messenger service sends a message directly to the recipient service and maintains a connection until the receiving service has completed processing and confirmed the successful processing.

Asynchronous Messaging

This is the preferred method for communicating between services. This entails utilising services such as EventBridge, SQS, SNS, Kinesis, etc based on what makes the most sense for your application. If possible, synchronous communication should always be reconsidered as asynchronous where possible.

There are a large number of benefits to using asynchronous methods:

  • Can reduce perceived application latency since frontend or client requests are not tied up in waiting for responses for synchronous messages or chains of synchronous messaging.
  • Reduces load on distributed systems as it allows the processing of messages to be deferred to such a time as there is capacity to deal with them
  • Further reduces load on distributed systems as allows for batch processing which improves efficiency of operations
  • Allows for control of the volume of traffic sent to third party systems or APIs that may have limits by allowing only a permitted amount of traffic per time period.
  • Reduces cost as you no longer have services that bill by the ms idling while waiting for responses.
  • Increases durability as the managed services managing the messages provide automated retry mechanisms should the receiving service suffer a failure of some type as well as the ability to move failed messages to dead letter queues that can notify a maintainer about the failure and allow for further retries if needed. 

Synchronous Messaging

Synchronous messaging includes communicating between services via an HTTP API or even direct Lambda invocation. This method should only really be used as a last resort if no asynchronous messaging method can be utilized. Some of the reasons why synchronous messaging is problematic:

  • There is no built-in retry mechanism. If a request is made to a downstream service that responds with error, the onus is on the requesting service to handle any failures or retries
  • Since the requesting service must wait for a response from the service being called, this can add unnecessary latency.
  • A receiving service or a third party service could become overwhelmed with requests since there is no control over the volume and since there is no automated retry mechanism there is the risk of transactions being lost.

Infrastructure Shared Between Services

In many situations, there will be some infrastructure that needs to be shared between services. These can include but are not limited to:

  • A shared API Gateway resource in order to ensure the same domain is used across all APIs.
  • A relational database.
  • A caching service of some kind such as RedShift, ElastiCache, etc
  • Kinesis streams
  • DynamoDB tables

When defining these pieces of infrastructure, it can be configured in CloudFormation using the resources section of a Serverless Framework service’s serverless.yml file (https://www.serverless.com/framework/docs/providers/aws/guide/resources). However, if it is shared between many services, it may not be clear which of the services is best suited for configuring these resources.

In this case, the recommendation is to create a separate Serverless Framework service expressly built for defining these shared resources. There are a number of reasons to do so:

  • Shared services such as these are often only ever deployed once and, compared to other services, rarely need to have deployments made in future.
  • If these shared services are removed from the more frequently deployed services there is also lower likelihood that they are accidentally updated
  • If changes do need to be made to any of these shared services, it becomes simpler to then discuss as a team what changes to make and when to deploy those changes into development environments to reduce disruption.
  • Changes of these services when pushed to production often have the likelihood of disruption and separating them in this way allows for greater control and planning for these deployments.

Git Repository Structure

Starting Out

The recommended repository structure for a Serverless application is a single monorepo that contains all individual services within it. There are a number of reasons for this:

  • When getting started, you are likely building an application using a single service or a very small number of services. Multiple repositories at this stage adds additional complexity for no immediate benefit.
  • Even as the number of services grows, a single monorepo makes keeping track of what already exists easier.
  • Onboarding new team members becomes much simpler with a single location for all details of the entire application.
  • If breaking up the monorepo is required for efficiency's sake later, it is far easier to break a single repo up into multiple repos than to merge them together into one.
  • Management of dependencies between services becomes simpler
  • It becomes easier to orchestrate deployments that span multiple services
  • It is easier to share code between services

As you grow

You may reach a point where the single monorepo is no longer appropriate. This is especially possible if the team size has grown substantially and particularly if the application under development and maintenance now consists of multiple distinct teams, each managing their own collection of services.

In this case, it can be beneficial to consider creating multiple repositories, one for each team, and moving the related services for those teams to their repositories.

Conclusion

Overall the summary for all of this is to maintain simplicity as long as possible until any added complexity can improve efficiency. Maintaining a single service for as long as possible has some large benefits, but eventually you may begin to reach the need for multiple services, and even those should be kept large enough to not add to the complexity of the application as a whole.

Messaging between multiple services should be as asynchronous as possible to preferably leverage the management and reliability features built into cloud services as opposed to synchronous methods that would require adding complexity with home grown retry techniques.

It’s good practice to maintain simplicity when managing multiple services by using a single monorepo for as long as possible.

Try Serverless Console

Monitor, observe, and trace your serverless architectures.
Real-time dev mode provides streaming logs from your AWS Lambda Functions.

Subscribe to our newsletter to get the latest product updates, tips, and best practices!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.