Auto-generate CloudFormation with no-code and no commitment 🚀
Try Now

Use case: message bus service with an audit log

Building a central message bus service with an audit log for microservices and leveraging the benefits of managed services.
Yev Krupetsky

September 2 2021 · 10 min read

SHARE ON

Use case: message bus service with an audit log

Have you ever tried building a system with microservices that communicate via messages? We know how quickly complications escalate when every service manages its own messaging. As the system grows, connections between dependent services become harder to maintain, and it's harder to make changes:

  • Adding new services is difficult – services need to change to communicate with the new service.
  • Making changes can break functionality – services become tightly coupled, and any change can cause an outage.
  • It's difficult to troubleshoot - messages are handled separately, making it difficult to track them in a central location.

Using a central message bus service

Instead of services talking to each other directly, we can extract the responsibility to receive and distribute messages to a dedicated service we'll call a message bus. It will host any number of pub-sub topics to which services can send and subscribe to receive messages.

By centralizing the messages traffic, we:

1. Eliminate tight coupling between services

Every service is free to send messages to one location instead of many. Adding new services doesn't require changing existing ones. Services subscribe to messages they care about without affecting others.

2. Prevent changes from affecting other services

When services change, they don't directly affect others. On the application, level workflows may be affected, which would have also happened with direct communication. But, when a receiving service changes, it doesn't break others who communicate with it, only itself.

3. Keep a central log of all messages passing in the system

Since all messages pass through a central location, they can be logged together for troubleshooting.

Resiliency & scalability

We should avoid turning the message bus service into a single-point-of-failure by designing it with fault tolerance and scalability in mind. The implementation details depend on the technology used. In this article, we use managed services with fault-tolerance and scalability built-in.

Building the service with Altostra

We'll build a message bus service on AWS to demonstrate one possible solution using managed services. We'll also have two services communicate with each other via the message bus as an example.

Building the infrastructure for our service by writing IaC templates like CloudFormation requires over 100 lines of code and a few hours of work. I'm taking into account the time it takes to look up the docs for each service, its properties and iterating to fix errors that arise after deploying.

With Altostra, it takes a few minutes to add resources, connect and configure them. It gives you a no-code designer and generates an AWS CloudFormation template when you deploy.

I invite you to create a free account and follow along. Alternatively, you can use the AWS SAM template generated by Altostra as a reference for the service we build in this article.

What we'll build

To demonstrate, we'll build a basic backend for an e-commerce website that handles incoming orders. It needs to:

  1. Quickly respond to clients for newly placed orders.
  2. Process and store orders and customers data in a database.
  3. Send notifications to customers.

The backend will be composed of three microservices:

  1. Client API — will receive order requests from clients and send messages to the message bus for further processing.
  2. Message Bus — will forward sent messages to subscribed services.
  3. Orders Processor — will process and store orders and customers data and send customer notifications.

Required knowledge: This article assumes you are familiar with the following AWS services: Lambda, SNS and SQS (including DLQ).

Architecture

Client API service

client api architecture

This service has:

  • An API with a single endpoint to handle new order requests from a client.
  • A Lambda function to validate and extract orders and customers details from requests and send messages for further handling.
  • Two existing SNS topics for orders and customers, referencing the topics in our message bus.

Message Bus service

message bus architecture

We'll use three SNS topics, one for each, Orders, Customers and Notifications. The Message Bus service hosts these SNS topics and makes them available for other services.

It will also have:

  • An SQS queue subscribed to all SNS topics.
  • A Lambda function to log all messages from the queue in a DynamoDB table.
  • A Dead Letter Queue (DLQ) for messages the Lambda function fails to log.

We could use a single SNS topic with message properties for routing instead of using three. By splitting them up, we provide other services better control over subscriptions—improving security and load handling.

Orders Processor service

order processor architecture

This service has:

  • An existing SNS topic for orders and customers, referencing the topics in our message bus.
  • An SQS queue for each topic to control message flow and handling.
  • A Lambda for each queue to handle messages and execute business logic.
  • An AWS Aurora database to store orders and customer data.
  • An existing SNS topic for notifications, referencing the topic in our message bus.

Setting up

We'll focus mainly on the Message Bus service. The Client API and the Orders Processor services are only there to serve our example.

To follow along, you'll need

Create the message bus project

We’ll start by creating a new empty Altostra project.

  1. On the projects page, click Create Project.
  2. Enter a name.
  3. Click Create.

Altostra will create the new project and a Git repository for it.

Open the project on your machine

Clone the project to the local machine and open it in Visual Studio Code:

  1. On the project page that you created, click Clone, select your preferred protocol and copy the link.
  2. Clone the repository to your local machine.
  3. Open the project directory in VS Code.
clone project

Login to Altostra CLI

The CLI requires authentication to work with your Altostra account.

Simply run:

$ alto login

Building the Message Bus service

For simplicity, I keep the default configuration for most of the resources and connections. You can make changes to suit your needs.

message-bus-service

Add the resources

  1. Add three SNS topics. Call them orders, customers and notifications.
  2. Add an SQS queue for the incoming messages. Call it messages.
  3. Add another SQS queue to serve as the DLQ. Call it failed-messages.
  4. Add a Lambda function to handle and store the messages. Call it store-messages.
  5. Add a DynamoDB table to store the messages log. Call it messages-log.

Connect the resources

  1. Connect all three SNS topics to the messages queue—this creates a subscription connection.
  2. Connect the messages queue to the failed-messages queue—this creates an errors connection that makes it the DLQ for the messages queue.
  3. Connect the messages queue to the store-messages Lambda function—this creates a trigger connection that will execute the Lambda for every message in the queue.
  4. Connect the store-messages function to the messages-log DynamoDB table—this creates an access connection. Click on the connection to edit, and change the access level to write-only since there's no reason for the function to have read access to the entire messages log.

Add code to the store-messages function

For our example, this function will only store messages. But it can do much more. It is crucial to also handle security, logging, configuration, error handling, etc., in production code.

const util = require('util')
const AWS = require('aws-sdk')

const ddb = new AWS.DynamoDB.DocumentClient()

const TableName = process.env.TABLE_MESSAGESLOGTABLE

module.exports.handler = async (event, context) => {
  console.log(`Handling message`, util.inspect(event, { depth: 5 }))

  const tasks = event.Records.map(record => {
    const body = JSON.parse(record.body)
    return ddb.put({
      TableName,
      Item: {
        'pk': new Date().toISOString(),
        'topicArn': body.TopicArn,
        'message': body.Message
      }
    }).promise()
  })

  await Promise.all(tasks)
}

Integrating the services

I already have the Client API and Orders Processor services ready. All I do is add and connect existing SNS topic resources from the Message Bus service. Here's how you can do it.

Client API service

  1. Add an existing SNS topic, call it orders.
  2. Click to enable Set as Parameter for the Identifier field. The field will change to hold a parameter name instead.
  3. Enter ORDERS_TOPIC as the Identifier Parameter Name.
  4. Repeat steps 1-3. This time name it customers and the Identifier Parameter Name CUSTOMERS_TOPIC.
  5. Connect the handle-new-order Lambda function to both topics—this creates an access connection allowing the function to post messages to the topic.

We'll use parameters for the topics' identifiers to reference them in environment configuration during deployment instead of hard-coding the topic IDs in the service design.

Orders Processor service

  1. Add an existing SNS topic, call it orders.
  2. Click to enable Set as Parameter for the Identifier field. The field will change to hold a parameter name instead.
  3. Enter ORDERS_TOPIC as the Identifier Parameter Name.
  4. Repeat steps 1-3. This time name it customers and the Identifier Parameter Name CUSTOMERS_TOPIC.
  5. Connect each topic to its respective SQS Queue—this creates a subscription connection. You can optionally set filters on the connections (by clicking on them) to filter messages based on attributes.

We'll add the parameters above to our environment after deploying the Message Bus service because the identifiers are generated only after deploying the service for the first time.

Deploying

Deploy the Message Bus service

In the Message Bus service's project directory, run:

$ alto deploy prod --env Lab --push

Wait for the deployment to finish. You can monitor the deployment status in the Web Console.

To monitor it from the CLI, run:

$ alto stack get prod
...
Name  Updated               Environment  Version                        Status
prod  9/1/2021, 2:54:54 PM  Lab          auto-2021-09-01T14-54-34.648Z  Deployed

Set environment parameters

To use the topics from our Message Bus service in other services, we need to add parameters with their IDs (ARN on AWS) to our target environment:

  1. Go to the Environments page and click on your target environment.
  2. Switch to the Parameters tab.
  3. Add parameters with the names we used above and their IDs:
setting parameters

Deploy the API Client and Orders Processor services

Now that we have the parameters set in the environment, we can deploy the rest of the services:

In the Client API service's project directory, run:

$ alto deploy prod --env Lab --push

In the Orders Processor service's project directory, run:

$ alto deploy prod --env Lab --push

Next steps

We provide a free-forever plan for developers. Create your free account today at https://app.altostra.com.

We want to hear from you! Share your Altostra experience with us on Twitter @AltostraHQ and LinkedIn and any suggestions that can make your work with Altostra even better.

Happy coding!

By submitting this form, you are accepting our Terms of Service and our Privacy Policy

Thanks for subscribing!

Ready to Get Started?

Get Started for Free

Copyright © 2021 Altostra. All rights reserved.