Serverless and Functions-as-a-Service

Functions to Tame Complexity

I promise we'll get to FaaS and serverless, but we have to see something more real than serving up a static page. Let's add a couple of features to our app.

Let's add a form, so that users can submit their email address to learn about our amazing website. We'll save that to a database and launch a background job that sends them a welcome email. We're not going to set up a real database or background processing system here, but we'll write code as if we have.

A More Realistic System

First, let's change js/server.js, as well as our function protocol to allow posting data. We'll keep it simple: if the HTTP method is a POST, we'll parse the body and send it to the function as an argument.

const http = require("http");
/* start new code */
const qs   = require("querystring");
/* end new code */
const app  = require("./app");

const hostname = "127.0.0.1";
const port     = 3000;

const server = http.createServer((req, res) => {

  const write = (allData) => {
    res.write(allData, () => {
      res.end();
    });
  }

  res.statusCode = 200;
  res.setHeader("Content-Type", "text/html");
/* start new code */
  if (req.method == "POST") {
    let body = [];
    req.on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      app(write,qs.parse(body));
    });
  }
  else {
    app(write);
  }
/* end new code */
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

This is mostly boilerplate Node stuff, so nothing revolutionary here. We need to change js/app.js, but first we need to add a signup form to our template and, while we're there, let's also add a place to render a confirmation message.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>The Serverless Compendium</title>
  </head>
  <body>
<!-- start new code -->
    <h3>##email##</h3>
<!-- end new code -->
    <h1>The Serverless Compendium</h1>
    <h2>An exhaustive tour of ƒ-as-a-service</h2>
    <p>
      Welcome to the next level of abstraction and
      scalability! As of <strong>##date##</strong>,
      there are <strong>##count## startups</strong>
      built entirely on functions!
    </p>
<!-- start new code -->
    <form action="/" method="post">
      <label>
        Sign Up
      </label>
      <input type="email" name="email" />
    </form>
<!-- end new code -->
  </body>
</html>

Now, we'll replace js/app.js with a version that handles the POST. Because we'll need more code, let's break it up into some functions. We'll create a function called renderPage that accepts the write function js/server.js gives us, as well as the parsed params (which could be null). This function's job is to replace the HTML template with dynamic data, so it'll look at params and, if there is a params.email, replace the newly-introduced ##email## placeholder with that email. Otherwise, it replaces it with the empty string.

We'll also make a function called emailSignup that, currently, just logs that someone has requested a signup, and then defers to renderPage. At the very end we'll call either emailSignup or renderPage, depending on if we have a parsed body from js/server.js or not.

const fs   = require("fs");
const path = require("path");
const index = fs.readFileSync(
      path.resolve(__dirname,"..","html","index.html")
    ).toString();

const renderPage = (write,params) => {
  var html = index.replace("##date##",(new Date()).toString()).
    replace("##count##",Math.floor(Math.random() * 1000));

  if (params && params.email) {
    html = html.replace("##email##",`Thanks ${params.email}`);
  }
  else {
    html = html.replace("##email##","");
  }
  write(html);
}

const emailSignup = (write,params) => {
  console.log(`Signing up ${params.email}`);
  renderPage(write,params);
}

module.exports = (write,body) => {
  if (body) {
    emailSignup(write,body);
  }
  else {
    renderPage(write);
  }
}

If we start our server, the app is still working, but rendering our sign-up form:

> node js/server.js

Updated App with Sign-up Form

If we submit the form, it works and re-renders the page:

Signed Up

OK, so what has this to do with events and functions? One more step before we're done. We want to store the email address in a database and then fire off a background job to send a welcome email to that address.

We'll fake this out by creating some classes. First, we'll make a very cheesy database:

class Database {
  constructor() {
    this.data = {};
    this.nextId = 1;
  }
  save(email) {
    const id = this.nextId;
    this.nextId++;

    this.data[id] = email;
    return id;
  }
  find(id) {
    return this.data[id];
  }
}
module.exports = (new Database());

We'll also make a cheesy background job system that just executes a function after 500ms:

class BackgroundJob {
  constructor(f) {
    this.job = f;
  }
  performLater(args) {
    setTimeout(() => { this.job(args) },500);
  }
}
module.exports = BackgroundJob

Now, we'll require them in js/app.js:

const fs   = require("fs");
const path = require("path");
const index = fs.readFileSync(
      path.resolve(__dirname,"..","html","index.html")
    ).toString();
/* start new code */

const Database      = require("./Database.js");
const BackgroundJob = require("./BackgroundJob.js");
/* end new code */

const renderPage = (write,params) => {
  var html = index.replace("##date##",(new Date()).toString()).
    replace("##count##",Math.floor(Math.random() * 1000));

  if (params && params.email) {
    html = html.replace("##email##",`Thanks ${params.email}`);
  }
  else {
    html = html.replace("##email##","");
  }
  write(html);
}

const emailSignup = (write,params) => {
  console.log(`Signing up ${params.email}`);
  renderPage(write,params);
}

module.exports = (write,body) => {
  if (body) {
    emailSignup(write,body);
  }
  else {
    renderPage(write);
  }
}

Armed with this powerful array of webscale subsystems, we can enhance emailSignup to save the email and queue the background job.

const fs   = require("fs");
const path = require("path");
const index = fs.readFileSync(
      path.resolve(__dirname,"..","html","index.html")
    ).toString();

const Database      = require("./Database.js");
const BackgroundJob = require("./BackgroundJob.js");

const renderPage = (write,params) => {
  var html = index.replace("##date##",(new Date()).toString()).
    replace("##count##",Math.floor(Math.random() * 1000));

  if (params && params.email) {
    html = html.replace("##email##",`Thanks ${params.email}`);
  }
  else {
    html = html.replace("##email##","");
  }
  write(html);
}

const emailSignup = (write,params) => {
  console.log(`Signing up ${params.email}`);
/* start new code */
  const backgroundJob = new BackgroundJob((emailId) => {
    const email = Database.find(emailId);
    console.log(`Sending welcome email to ${email}`);
  });
  const id = Database.save(params.email);
  console.log(`${params.email} saved to the DB with id ${id}`);
  backgroundJob.performLater(id);
/* end new code */
  renderPage(write,params);
}

module.exports = (write,body) => {
  if (body) {
    emailSignup(write,body);
  }
  else {
    renderPage(write);
  }
}

Restart the server:

> node js/server.js

Submit the form, which should still work fine:

Signed Up

And we should see our log messages:

Server running at http://127.0.0.1:3000/
Signing up pat@example.com
pat@example.com saved to the DB with id 1
Sending welcome email to pat@example.com

With that in place, we have a more complex system to reason about, so let's take a fresh look at it as a series of events.

In Which Events Become More Clear

Although our code is very procedural, the overall processes we're implementing aren't as coupled as it would seem. If we were to look at our system through the lens of causes and effects, we might find it looks something like this:

Our system as a series of events
Click to embiggen

The arrows in this diagram are events, and the boxes are functions that get called based on those events. This shows the logical flow of what is triggering what, but omits all of the technical details (for example, this doesn't mention background jobs since that's an implementation detail—not directly relevant to the problem).

Let's re-design our fledgling system to be based around events and reactions to those events.

First, let's create functions for each of the steps. We'll need a function that renders the page (which we largely have already in renderPage), one to store an email address in the database, and one to send a welcome email.

Instead of doing that inline, we'll make files for these. First up, renderPage, which is almost identical to the current function. One change we'll make is to replace the explicit params with a general params object. We'll expect to find params.write in it, and optionally params.body:

const fs   = require("fs");
const path = require("path");
const index = fs.readFileSync(
      path.resolve(__dirname,"..","html","index.html")
    ).toString();

/*
 * @param params an object expected to have two keys:
 *        - write - the function passed by js/server.js
 *        - body - the parsed body of the request (optional).
 */
const renderPage = (params) => {
  var html = index.replace("##date##",(new Date()).toString()).
    replace("##count##",Math.floor(Math.random() * 1000));

  if (params.body && params.body.email) {
    html = html.replace("##email##",`Thanks ${params.body.email}`);
  }
  else {
    html = html.replace("##email##","");
  }
  params.write(html);
}
module.exports = renderPage;

Next, we need a function to store the email in the database. We'll have it take a general params object as well:

const Database      = require("./Database.js");
/*
 * @param params - an object expected to have the key `body`, which
 *                 is itself an object expected to have the key
 *                 `email`.
 */
const storeInDatabase = (params) => {
  const email = params.body.email;
  console.log(`Signing up ${email}`);
  const id = Database.save(email);
  console.log(`${email} saved to the DB with id ${id}`);
}
module.exports = storeInDatabase;

Lastly, we'll create a function that sends a welcome email (which we hand-wave over). This takes a seemingly convoluted data argument that we'll explain in a bit.


/*
 * data - an object expected to have a key `email`, that
 *        should be an object with the key `data`, that
 *        should be an object with the key `email` (I know),
 *        that should be an email address.
 */
const sendWelcomeEmail = (data) => {
  const email = data["email"]["data"]["email"];
  console.log(`Sending welcome email to ${email}`);
  return email;
}
module.exports = sendWelcomeEmail;

To make our system work, we need to arrange for these functions to respond to the right events. This means we need some way to fire events and register these functions as reactors to those events.

Let's create a simple event bus class to do that:

class EventBus {
  constructor() {
    this.eventListeners = {};
  }
  /**
   * Register a listener with an arbitrary event.
   *
   * @param eventName - the name of the event.  Can be anything.
   * @param listener  - the event listener.  Should be a
   *                    function and will be given whatever
   *                    params were given to fire
   */
  register(eventName,listener) {
    if (!this.eventListeners[eventName]) {
      this.eventListeners[eventName] = [];
    }
    this.eventListeners[eventName].push(listener);
  }
  /**
   * Fire an event and notify any listeners.

   * @param eventName - name of the event.
   * @param params    - an object representing the parameters
                        relevant to the event.
   */
  fire(eventName,params) {
    if (!this.eventListeners[eventName]) {
      console.log(`No listeners for ${eventName}`);
      return;
    }
    this.eventListeners[eventName].forEach( (listener) => {
      listener(params);
    })
  }
}
module.exports = (new EventBus());

This isn't anything special, it just stores listeners on events and provides a way to fire events to notify those listeners.

Part of making this all come together as events is to have each piece of the app fire relevant events when interesting things happen. We want our Database to do this whenever it saves an email. This is where we see why the data parameter is so complex. We're trying to make a generic event here, that looks like so:

{
  "«table name»": {
    "action": "«action name, e.g. created»",
    "data": {
      "«some field in the DB»": "«its value»",
      "«some other field»": "«its value»"
    }
  }
}

In our case, the data that goes with the event looks like this:

{
  "email": {
    "action": "created",
    "data": {
      "id": 42,
      "email": "dave@example.com"
    }
  }
}

Here are the code changes to make that happen:

/* start new code */
const EventBus = require("./EventBus.js");

/* end new code */
class Database {
  constructor() {
    this.data = {};
    this.nextId = 1;
  }
  save(email) {
    const id = this.nextId;
    this.nextId++;

    this.data[id] = email;
/* start new code */
    EventBus.fire("newEmailAddress",{ email: { action: "created", data: { id: id, email: email }}});
/* end new code */
    return id;
  }
  find(id) {
    return this.data[id];
  }
}
module.exports = (new Database());

With these pieces, js/app.js becomes not much more than configuration. We need to register the functions we just created as receivers for some events. One event is newEmailAddress, which we have the Database firing. The other two events we'll fire inside js/app.js as well:

const EventBus         = require("./EventBus.js");
const renderPage       = require("./renderPage.js");
const storeInDatabase  = require("./storeInDatabase.js");
const sendWelcomeEmail = require("./sendWelcomeEmail.js");

EventBus.register("emailSignup"     , storeInDatabase);
EventBus.register("emailSignup"     , renderPage);
EventBus.register("pageRequested"   , renderPage);
EventBus.register("newEmailAddress" , sendWelcomeEmail);

module.exports = (write,body) => {
  if (body) {
    EventBus.fire("emailSignup",{ write: write, body: body});
  }
  else {
    EventBus.fire("pageRequested", { write: write });
  }
}

If we restart our server, navigate to the page, and submit our email, everything still works:

> node js/server.js

Signed Up

Our log messages prove that this is happening:

Server running at http://127.0.0.1:3000/
Signing up pat@example.com
Sending welcome email to pat@example.com
pat@example.com saved to the DB with id 1

So, now what? There's a lot going on in what we just did. Certainly, functional decomposition and putting stuff in different files helps keep our code organized, but all the logic of what happens during sign up is gone!

What we've done has some nice benefits:

This does have a big downside, which is that it's now pretty difficult to piece together what happens when a user submits their email address to us. But, I'd argue that from the configuration that is now inside js/app.js, this information could be mechanically derived.

So, what about serverless architecture and FaaS?

What if you didn't have to own the orchestration code?

When deploying a system that runs inside a web server like Node, or even a more sophisticated framework like Ruby on Rails, you still end up with a lot of what is essentially configuration and orchestration—getting all the little pieces to work together.

In the app we've created, js/server.js and EventBus are the orchestration, and js/app.js is just configuration—what events are wired to what services. What if we didn't have to directly manage this? What if the orchestration was provided by a cloud services provider, and we gave them our configuration (instead of writing it ourselves)?