Build a serverless Event Sourcing application with Google Firebase

This guide will show how to get started building a serverless Firebase application using Serialized to store your events.

Create a Firebase project and setup configuration

Follow steps 1-4 in the instructions on Firebase get-started guide to configure your project to use cloud functions.

We selected Typescript as the language for this example but you can also use Javascript if you prefer that.

Install axios http client

Before implementing the cloud function that will contain our business code we need to install an HTTP client to call Serialized APIs. For this we choose axios.

cd functions && npm install axios

We also need to configure the axios client with the credentials to your Serialized project.

firebase functions:config:set serialized.accesskey="<YOUR_ACCESS_KEY>" 
firebase functions:config:set serialized.secretaccesskey="<YOUR_SECRET_ACCESS_KEY>"

To create the axios client with the configured credentials and correct default headers, let’s create a client.ts file that we put in the /functions directory.

const functions = require('firebase-functions');
const axios = require('axios');

const accessKey = functions.config().serialized.accesskey;
const secretAccessKey = functions.config().serialized.secretaccesskey;

const axiosInstance = axios.create({
  baseURL: 'https://api.serialized.io',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Serialized-Access-Key': accessKey,
    'Serialized-Secret-Access-Key': secretAccessKey
  }
});

export default axiosInstance;

Implementing addMessage() using Serialized

The Firebase get started guide implements an addMessage() method which stores messages in Firestore. We will implement the same method but we will instead use Serialized as the database for these messages.

We will store each message in a separate aggregate. Serialized require an id for all aggregates which should be a UUID, so we’ll install the uuid npm package.

npm install uuid && npm install @types/uuid

Now let’s implement the cloud function. Open index.ts and add the following code:

exports.addMessage = functions.https.onRequest(async (req, res) => {

  // Generate a random message ID
  const messageId = uuidv4();

  // Extract the text from the request
  const messageText = req.query.text;

  // Create the event data structure
  const MessageAdded = {
    eventType: 'MessageAdded',
    data: {text: messageText},
  }

  // Wrap the event in an array since events are always sent in batches
  const eventBatch = {
    events: [MessageAdded],
    expectedVersion: 0,
  };

  // Store the event in Serialized
  await client.post(`aggregates/messages/${messageId}/events`, eventBatch);

  res.send(messageId);
});

Upgrade Firebase project

Since Serialized is not a Google product we need to upgrade our Firebase project to Blaze plan. It however includes a free tier so you can try it our without incurring any costs.

Deploy to Firebase

Deploy the cloud function to Firebase by calling firebase deploy:

firebase deploy --only functions

Watch the output where you should have a public URL where the cloud function is deployed.

Call the function

We can now test the function. Open the url provided from the deploy step in your browser and add the text query parameter.

For example:

https://us-central1-yourfirebaseserializedexample.cloudfunctions.net/addMessage?text=yourmessage

You should get the id of the new aggregate as a response:

460d5900-8257-4765-8b3b-ce0456ea462d

Record this id for later since we’ll need it when we’ll update this aggregate.

Loading and updating the aggregate

Updates in an event sourced application are different from a traditional application. A modification means adding another event to the list of all existing events for that aggregate. In our example we will make the message uppercase, by implementing a makeUppercase function that will emit an MessageUppercased event once for the aggregate. Subsequent calls to the method should not emit any events since the message now already has been uppercased.

This function will need to load the aggregate and a good way to represent this is to create a class in our code that will be loaded from the events that have been stored.

Add the following class to your index.ts file

class MessageAggregate {

  private text?: string;
  private hasBeenUppercased: boolean = false;
  private version: number = 0;

  static loadFrom(aggregate: any): MessageAggregate {
    const messageAggregate = new MessageAggregate();
    messageAggregate.version = aggregate.aggregateVersion;
    aggregate.events.forEach((e: any) => messageAggregate.handleEvent(e))
    return messageAggregate;
  }

  // Load aggregate from saved events
  handleEvent(event: any) {
    if (event.eventType === 'MessageAdded') {
      this.text = event.data.text;
    } else if (event.eventType === 'MessageUppercased') {
      this.text = event.data.text;
      this.hasBeenUppercased = true;
    } else {
      throw Error('Unknown event type ' + event.eventType);
    }
  }

  // Method that will make the text uppercase if it has not been called before.
  public makeUppercase(): any[] {
    if (!this.hasBeenUppercased) {
      const uppercasedEvent = {
        eventType: 'MessageUppercased',
        data: {text: this.text?.toUpperCase()}
      }
      return [uppercasedEvent];
    } else {
      return [];
    }
  }

  public getAggregateVersion() {
    return this.version;
  }

  public getText() {
    return this.text;
  }
}

The makeUppercase function will use the optimistic concurrency support provided by Serialized to assert idempotency of subsequent calls to it so that we don’t record multiple MessageUppercased events.

Add the following function to your index.ts file

exports.makeUppercase = functions.https.onRequest(async (req, res) => {

  // Get the id from the request
  const messageId = req.query.messageId;

  // Load the aggregate
  const messageAggregate = MessageAggregate.loadFrom((await client.get(`aggregates/messages/${messageId}`)).data);

  // Apply our business logic
  const updateEvents = messageAggregate.makeUppercase();
  if (updateEvents.length > 0) {
    // Write the uppercase event, making sure the version is intact (no concurrent modification).
    const eventBatch = {
      events: updateEvents,
      expectedVersion: messageAggregate.getAggregateVersion(),
    };
    await client.post(`aggregates/messages/${messageId}/events`, eventBatch);

    // Respond with the uppercased event
    res.send('Events stored: ' + JSON.stringify(updateEvents));
  } else {
    // Respond with 'Nothing to do' since the message already has been uppercased
    res.send('Nothing to do');
  }
});

Now we’re ready to deploy the MessageAggregate class and the makeUppercase function to Firebase:

firebase deploy --only functions

To test this new function, open the browser at: https://us-central1-yourfirebaseserializedexample.cloudfunctions.net/makeUppercase?messageId=<MESSAGE_ID_RECORDED_FROM_ADD_MESSAGE>

You will now get a response payload like this:

[{"eventType":"MessageUppercased","data":{"text":"YOURMESSAGE"}}]

If you try calling the method again you will receive the following response:

Nothing to do

Summary & next steps

This is a simple example of how to integrate a Firebase cloud function with Serialized Aggregate API. For a more complete application, we need to add proper error handling, projections for reading data and possibly reactions to drive workflows.

Read more about the details of Serialized Aggregates API in our documentation.

Good luck!