Hello Serverless: What Are We Building

Beautiful divider with lightning bolt in the middle
 

This is Part 1 of the series Hello Serverless, a guide aimed at the individual or team that is building their first serverless application. This series addresses the common conceptual and tactical questions that occur during the serverless development and operational lifecycle. This post covers the requirements and functionality of the Hello Serverless application used through the series.

Before we start building our application, we need to understand what we’re building. This post will provide the necessary context to the application that will drive engineering decision making. Here we’ll discuss where the Hello Serverless application exists in a broader application environment, its requirements, and its functionality. Finally, we’ll outline how to break down the application’s code. We’ll use these distinctions in future posts to help illustrate the evolution of the Hello Serverless application.

Application Story & Requirements

What is the Hello Serverless application? Previously we’ve said it’s a Hello World CRUD application. The application receives JSON data with a message key that has a value of “Hello World” (of course, that doesn’t have to always be the value). But that doesn’t really tell us much about what needs to be built, so let’s give it a little bit of a story.

The Hello Serverless application is a service in a much larger application. Another service or services in the larger application sends message data to Hello Serverless, which may at a later date be updated or deleted. There’s also another system or systems that retrieve message data from Hello Serverless. And in the future, we may want to enrich the data that Hello Serverless manages.

We should make some important notes about data operations. First, our data doesn’t need to be processed in order of creation. No message depends on a previous message having been processed. Second, the relationship between data create and update operations, and retrieve operations, is not time sensitive. A system attempting to retrieve data is not dependent on expecting the data has already been created. Instead the system will try again and any delay in data retrieval is acceptable. Finally, unlike the system(s) retrieving data, the system(s) creating, updating, and deleting data don’t need to be informed that the operation was completed successfully. Typically with a RESTful web API you would return an HTTP 200 status code, or an error status code, to the client so the client knows if the operation needs to be retried or because possibly the client needs to notify another system that the operation has been completed. For retries, let’s assume an operation needs to succeed but we don’t have a requirement on how we ensure the operation succeeds. Also we’ll assume another system does not need to be notified of operation completion.

Application Functionality

The application we’re building accepts an HTTP POST request to the /message endpoint with a JSON doc that has a single attribute named message and a value that is an arbitrary string. Here’s an example of what the data might look like.

{
  "message": "Hello World"
}

Once the message is received, a key named pk, short for primary key, with a random UUID as a value is added to the document and the document is put into DynamoDB.

You’ll also see sk, our sort key, with a value of v0 that will be static on all items. A sort key combined with a primary key creates a composite key, which is used when you have a non-unique primary key. A common example of this is a recording artist as a primary key value and albums as a sort key value. But we said earlier that the pk would be a unique UUID, so why have a sort key? We’ll explain that shortly.

{
  "message": "Hello World",
  "pk": "8806565e-b05b-4980-acb3-582ae2235ebc",
  "sk": "v0"
}

After the data has been inserted a response body is returned to the client and looks like this:

{
  "message_id": "8806565e-b05b-4980-acb3-582ae2235ebc"
}

Note that instead of pk, the UUID’s key name is message_id. We’ll skip over a deep dive into why and just point out that as a NoSQL database, DynamoDB can hold multiple data shapes. That means the value of pk in a table might not always reflect a message ID. And in future blog posts it won’t. So while we externally present the UUID as a message ID, internally we refer to it as a generic primary key.

Now let’s come back to our sort key, sk, and why we use it with pk to form a unique composite key when the value of pk is unique. Understanding what you’re trying to build now is hard enough. Understanding what you might want to build in the future is impossible. Just like our primary key is named pk and not messsage_id in case we have future requirements, sk with a static value exists to handle possible future requirements. You cannot add a table sort key to a DynamoDB table in the future without creating a new table and performing a data migration from the old table to the new. So for example, one future feature we might add could be message translation support where the sort key is the translation language. To add this feature we would either have to create a second table for the application or create a new table and migrate existing data to the new table. I am increasingly of the opinion that your DynamoDB table should always have a sort key with a static value if its existence is not necessary for your initial data.

Now, given a value of message_id you can retrieve, update, or delete an item by making a request to /message/<message_id> using the appropriate HTTP method along with any request data if required.

To sum up our application, here are the different endpoints and what they do.

  • HTTP POST: /message: Create a message item from the JSON doc posted to the endpoint.
  • HTTP GET: /message/{message_id}: Retrieve the given message_id.
  • HTTP PUT: /message/{message_id}: Update the given message_id with the data from the JSON doc sent to the endpoint.
  • HTTP DELETE: /message/{message_id}: Delete the given message_id.

 

Application Code

In order to build this application, we have to write code to handle different bits of functionality. We can actually generalize the different areas of code that need to be written because most of it will appear one way or another in each iteration of this application. The code itself is intentionally very simple because we shouldn't be getting hung up on what it is doing or how it is doing it.

Instead, we should focus on how the code changes, moves, disappears, or even stays the same. This should help someone new to serverless but familiar with writing RESTful APIs in a language and framework like Python and Flask or JavaScript and Express to grasp building a serverless application. It should also help them to understand how they might go about migrating an existing application to a serverless architecture.

Let’s dive into the four different areas of our code.

Application Initialization

Application initialization is the application entry point. Initialization code creates the application instance and provides an interface from the HTTP web server to the application. Because they’re two different concerns, I usually split the code into two different files, and in a more complicated application, it would be more obvious why.

Route Definitions

Route definitions are where the application’s HTTP endpoints are defined. These tell us for example that the application takes a POST request to the /message endpoint or a GET to /message/<message_id>, where the last part of the path is the message ID the client wishes to retrieve.

Route definitions do not directly define request logic to execute. Instead they define the request logic function that will execute when a request is received. It’s a subtle distinction that will become more apparent as the application evolves.

Request Logic

If route definitions define an endpoint, request logic is what will execute when that endpoint receives a request. The only purpose of the request logic in my code is to receive the request, extract the necessary data, and pass it to the appropriate business logic. Route definitions and request logic can be found in the same file.

In the future, once we’ve gone fully serverless we’ll instead refer to this as event logic to reflect the event-driven nature of serverless functions.

Business Logic

Finally, our business logic is where the actual work our client wants to accomplish happens. This is the code that will handle creating, retrieving, updating, and deleting messages in DynamoDB. I separate business logic from route logic, which will appear like overkill for such a simple application. However, it’s to illustrate a point that will become apparent as the application evolves throughout this series. Making this separation will help you in migrating an application to serverless, and in evolving future serverless applications when necessary.

Building Hello Serverless

When talking with people building their first serverless application, often their first foray is building a RESTful web service. This is a natural first choice because it is what they are accustomed to building. That doesn’t mean it’s the most appropriate architectural choice, but I think it’s worth starting there and evolving. Starting with something that is well understood makes learning something new easier. You have the opportunity to compare and contrast similarities and differences between what you know and what you are learning.

What we will do in this series is start Hello Serverless with a Python Flask application and evolve it into a serverless monolith and then serverless functions. Finally, we’ll make one last change to go from synchronous to asynchronous events. Noting how the application code changes, moves, disappears, or even stays the same will help you to better understand what a serverless application may look like.

Continue on to Part 2 of the series, Building A Python Flask Application. There we'll show what this application might look like if we were not building a serverless application. It will help us better understand our serverless migration by starting with something already familiar.

Contact Us

Looking to get in touch with a member of our team? Simply fill out the form below and we'll be in touch soon!