DEV Community

Cover image for EventBus Mesh
Benjamen Pyle for AWS Community Builders

Posted on • Edited on • Originally published at binaryheap.com

EventBus Mesh

I've been thinking about this topic a lot lately when bringing EventBridge's EventBus into some applications. On the current projects I'm working on with existing code, I've said 100 times, if EventBridge existed when I started them, I wouldn't have so much SNS->SQS based code lying around. But such is life when working in evolving tech. Enter the EventBus Mesh

Microservice items of owernship

I want to start with the idea that in a Microservice architecture, I like to have everything be as independent as possible. That means the following items are each owned by the boundary itself

  • API Definition
  • Data model and data store
  • Frontend and Backend code
  • Any async communication channels
    • A way to publish that only that boundary to use
    • A way to subscribe that only that boundary can use

Pub/Sub before EventBridge

Before EventBridge, I built a lot of software that looked like the one below. And for clarity, there is 100% nothing wrong with this approach

Traditional Pub-Sub

  • Functions behind API Gateway
  • A single or multiple DynamoDB Tables under its control
  • SQS for subscribing to other SNS Topics (only it can read from)
  • SNS Topic for publishing messages/events out to the ecosystem. Only it could publish on that topic

With the advent of EventBridge Pipes and the ability of EventBridge Rules to trigger a Step Function (like this pattern EventBridge Step Function Rule), I felt the need to explore replacing SNS/SQS with EventBridge for new features and projects.

Again, nothing wrong with any of the above but I wanted to gain some additional capabilities

Benefits of Moving to EventBridge

  • Rules - they are more expressive and powerful than simple SNS message filtering. Filtering saves execution cycles which saves on cost and saves on waste
  • Schema Discovery - really nice feature to have messages/events be expressed through Schema
  • Pipes - standalone, they are fantastic for filtering, enriching and transforming. For more on Pipes, have a read here

With just these three benefits, I save on wasted execution, remove points of failure and eliminate Lambdas and other code that could introduce errors that have to be tested. This old article by Jeff Barr which describes the famous Keynote that Jeff Bezos did where he talks about "undifferentiated heavy lifting" is an example why I like pushing this kind of code up to the cloud

What is an EventBus Mesh

I drew up this simple diagram to highlight how I see putting Buses together.

EventBus Mesh

Some of the core pieces of this pattern are this

  • Functions respond to API requests
  • Functions (Lambda or Step) interact with DynamoDB
  • EventBridge becomes the glue
    • Service A Bus connects to Service B Bus
    • Service A Bus connects to Functions in Service A
    • EventBridge Pipes are used in connecting to DynamoDB on Service A and then are registered onto Service A Bus

The really important thing to nail down is the "boundaries". It's important to make sure that what owns what is isolated to that boundary. The real glue is Bus -> Bus. It's like Pub/Sub (and it is) but instead of subscribing an SQS Queue to an SNS Topic, you subscribe Service B's Bus to Service A's bus. Additionally, make sure to specify the Rules around the data that will connect those buses.

The big con for me is that Bus B when connected to Bus A knows about Bus A. But if Queue B knows about Topic A, in my mind it's about the same thing. And in the case where this is all internal to one collection of products, it doesn't bother me so hopefully, it doesn't bother you as well.

Two Bus Setup

For this example, we are going to build two EventBuses and connect them. Very similar to the diagram above.

Let's look at some CDK code to build

EventBus One

export class EventBusOne extends Construct {
    private readonly _bus: EventBus;

    constructor(scope: Construct, id: string) {
        super(scope, id);

        this._bus = new events.EventBus(scope, "EventBusOne", {
            eventBusName: "event-bus-one",
        });
    }

    get eventBus(): EventBus {
        return this._bus;
    }
}
Enter fullscreen mode Exit fullscreen mode

EventBus Two

export class EventBusTwo extends Construct {
    private readonly _bus: EventBus;

    constructor(scope: Construct, id: string) {
        super(scope, id);

        this._bus = new events.EventBus(scope, "EventBusTwo", {
            eventBusName: "event-bus-two",
        });
    }

    get eventBus(): EventBus {
        return this._bus;
    }
}
Enter fullscreen mode Exit fullscreen mode

When that gets run, the resources created will look like this in the console.

EventBuses

With a foundation of two EventBuses, let's take a look at connecting them together.

Create the EventBus Mesh with a Rule

For this example, let's assume that Bus2 is interested in Events that land on Bus one that have the detail-type of "Busing". With CDK and TypeScript, we do that by this code.

    private buildBusOneRule = (
        scope: Construct,
        props: EventBridgeRuleStackProps
    ) => {
        const rule = new Rule(this, "BusOne-BusTwo-Rule", {
            eventPattern: {
                detailType: ["Busing"],
            },
            ruleName: "bus-two-mesh",
            eventBus: props.busOne,
        });

        const dlq = new Queue(this, "BusOneBusTwoMesh-DLQ");

        const role = new Role(this, "BusOneBusTwoMesh-Role", {
            assumedBy: new ServicePrincipal("events.amazonaws.com"),
        });

        rule.addTarget(
            new targets.EventBus(props.busTwo, {
                deadLetterQueue: dlq,
                role: role,
            })
        );
    };
Enter fullscreen mode Exit fullscreen mode

When connecting the buses together the output of that will look like the below in the console.

Connecting Bus One to Bus Two

I like using the naming convention of "mesh" in the name just so I know the Rules that are related to connecting Buses easily stand out. And they could be searched on that keyword.

BusTwo Rules for Triggering Targets

Now that BusTwo is receiving events with detail-type of "Busing", it only makes sense to have that event go somewhere. In a real scenario, I might be responding to change in one system and needing to handle that change in another. By using EventBus and Rules you can get granular. For instance

  • Only events that have a certain detail-type go to a specific State Machine
  • Look for multiple detal-type that go to the same State Machine
  • Look for matches in the payload itself
  • So many other choices that can be explored here

Things really scale well and can be extended to your actual liking.

private buildBusTwoRule = (
        scope: Construct,
        props: EventBridgeRuleStackProps
    ) => {
        const rule = new Rule(this, "SampleEventSM-Rule", {
            eventPattern: {
                detailType: ["Busing"],
            },
            ruleName: "bus-two-busing",
            eventBus: props.busTwo,
        });

        const dlq = new Queue(this, "SampleEventSM-DLQ");

        const role = new Role(this, "SampleEventSM-Role", {
            assumedBy: new ServicePrincipal("events.amazonaws.com"),
        });

        rule.addTarget(
            new targets.SfnStateMachine(props.stateMachine, {
                input: RuleTargetInput,
                deadLetterQueue: dlq,
                role: role,
            })
        );
    };
Enter fullscreen mode Exit fullscreen mode

The above code should look a lot like the code that connects the EventBuses but the Rule's target is now a StateMachine and not another EventBus.

When run and deployed up to the Cloud, the Console will show you that the rule presents like this.

Rule Event Pattern

Bus Two Rule

Rule Target

Bus Two Target

Executing the Target of the EventBus Mesh

The whole point of connecting two or more buses is so that a consumer of one of the buses can do something with the updated information or the command that was put on the wire.

In the case of this demonstration, I'm going to use Step Functions with a simple State Machine. Taking a quick look at what's deployed, you'll see that it's a single step. Of course, you'd want something more robust if you handled this event, but this is just a demo and this article is not on Step Functions

EventBus Mesh State Machine

/**
 * Sets up the state machine. Brings in the roles, permissions and appropriate keys and whatnot
 * to allow the state machine to do its thing
 *
 *  @param {Construct} scope - the context for the state machine
 */
finalizeStateMachine = (scope: Construct) => {
    const logGroup = new logs.LogGroup(this, "CloudwatchLogs", {
        logGroupName: "/aws/vendedlogs/states/sample-state-machine",
    });

    const role = new Role(this, "StateMachineRole", {
        assumedBy: new ServicePrincipal("states.us-west-2.amazonaws.com"),
    });

    const flow = this.buildStateMachine(scope);

    this._stateMachine = new stepfunctions.StateMachine(this, "StateMachine", {
        role: role,
        stateMachineName: "SampleStateMachine",
        definition: flow,
        stateMachineType: stepfunctions.StateMachineType.EXPRESS,
        timeout: Duration.seconds(5),
        logs: {
            level: LogLevel.ALL,
            destination: logGroup,
            includeExecutionData: true,
        },
    });
};

/**
 * Creates the workflow for the state machine.  Builds transitions and errors/catches/retries
 *
 *  @param {Construct} scope - the context for the state machine
 */
buildStateMachine = (scope: Construct): stepfunctions.IChainable => {
    return new Succeed(scope, "We made it and it finished");
};
Enter fullscreen mode Exit fullscreen mode

Wrap Up

Hopefully, you've seen a touch of the power of this pattern. By using an EventBus Mesh you gain the ability to connect multiple features and then gain the isolation and control of publishing and reading those events while also using Pipes to help filter, enrich and transform if needed.

With most things Serverless and AWS there are many different ways to solve problems but using EventBridge Meshes should be a new pattern that you can now introduce into your toolkit.

And as always, if you want to see this code in action, feel free to pull it down from the Github repos. Then you can run

Prep the Environment
npm install
Enter fullscreen mode Exit fullscreen mode
Deploy EventBus Mesh
cdk deploy
Enter fullscreen mode Exit fullscreen mode
Destroy EventBus Mesh
cdk destroy
Enter fullscreen mode Exit fullscreen mode

Happy building!

Top comments (0)