# Building an Event-Driven Application with AWS AppSync, EventBridge, and Step Functions

Over the past years, event-driven architecture has become more and more popular. With the arrival of services such as EventBridge and its integration with many other AWS services, building such applications has become easier than ever.

Last year, [AWS announced support for Amazon EventBridge as a Data Source for AWS AppSync](https://aws.amazon.com/about-aws/whats-new/2023/04/aws-appsync-publishing-events-amazon-eventbridge/), and more recently, it was the turn of [EventBridge to integrate with AppSync](https://aws.amazon.com/about-aws/whats-new/2024/01/amazon-eventbridge-appsync-target-buses/). The two-way integration between those two services opens many opportunities.

In this article, I will show you how you can create an event-driven system with real-time updates using AWS AppSync, EventBridge, and Step Functions.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">To maintain brevity, this article assumes a certain level of familiarity with those services.</div>
</div>

## What We Will Build

We will build a simple food-ordering service where users can place an order for their meal. The order will be received by the back-end through an AWS AppSync GraphQL API. The Orders service will process it and send real-time updates to the user about the different status updates (e.g. Paid, Preparing, Out for delivery, Delivered, etc.). The Orders service is a Step Functions workflow that orchestrates the different steps of the process, while Amazon EventBridge asynchronously coordinates all the services.

Here is a high-level diagram of what this looks like.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1708065692079/0d25c9c7-bf1a-4bf9-b8b2-1720d11ad17c.png align="center")

To build this, we will use the CDK as our Infrastructure as Code (IaC).

<div data-node-type="callout">
<div data-node-type="callout-emoji">👨‍💻</div>
<div data-node-type="callout-text">Just want to see the code? You can find the <a target="_blank" rel="noopener noreferrer nofollow" href="https://github.com/bboure/serverless-eda-graphql-api" style="pointer-events: none">final solution on GitHub</a>.</div>
</div>

## The AppSync API

First, we'll need an AppSync API. It will be the entry point for our users to place orders. Here is the simplified schema.

```graphql
type Product {
  name: String!
  quantity: Int!
}

type Order {
  id: ID!
  status: OrderStatus!
  products: [Product]!
  createdAt: AWSDateTime!
  updatedAt: AWSDateTime!
}

input ProductInput {
  name: String!
  quantity: Int!
}

input CreateOrderInput {
  products: [ProductInput!]!
}

input NotifyOrderUpdatedInput {
  id: ID!
  status: OrderStatus!
  updatedAt: AWSDateTime!
}

type OrderUpdated @aws_api_key @aws_iam {
  id: ID!
  status: OrderStatus!
  updatedAt: AWSDateTime!
}

enum OrderStatus {
  PENDING
  PAID
  PREPARING
  OUT_FOR_DELIVERY
  DELIVERED
  CANCELLED
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order
  notifyOrderUpdated(input: NotifyOrderUpdatedInput!): OrderUpdated! @aws_iam
}

type Subscription {
  onOrderUpdated(id: ID!): OrderUpdated
    @aws_subscribe(mutations: ["notifyOrderUpdated"])
}
```

The schema contains 2 mutations:

* `createOrder`: can be used by the front end to place an order
    

This mutation is attached to a [pipeline resolver](https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L55-L80) with two functions: `createOrder` and `putEvent`.

`createOrder` uses a DynamoDB data source to persist the order in a table, and prepares an `order.created` event containing its details before storing it in the stash.

```typescript
// createOrder resolver
import { util } from '@aws-appsync/utils';
import { put } from '@aws-appsync/utils/dynamodb';

export const request = (ctx) => {
  const order = {
    id: util.autoId(),
    status: 'PENDING',
    products: ctx.arguments.input.products,
    createdAt: util.time.nowISO8601(),
    updatedAt: util.time.nowISO8601(),
  };

  return put({
    key: {
      id: order.id,
    },
    item: order,
  });
};

export const response = (ctx) => {
  // Here, we prepare the `order.created` event for the next step
  ctx.stash.event = { detailType: 'order.created', detail: ctx.result };

  return ctx.result;
};
```

`putEvent` is attached to an EventBridge data source. It receives an event (from the stash) and puts it into the event bus. We'll see later how it is used.

```typescript
// putEvent resolver
import { util } from '@aws-appsync/utils';

export function request(ctx) {
  const { event } = ctx.stash;

  if (!event) {
    console.error('No event found in stash');
    util.error('InternalError');
  }

  return {
    operation: 'PutEvents',
    events: [
      {
        source: 'order.api',
        ...event,
      },
    ],
  };
}

export function response(ctx) {
  return ctx.prev.result;
}
```

* `notifyOrderUpdate` will be used by the back end to send real-time updates.
    

It is attached to a [resolver with a *None* data source](https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L82-L88) because this mutation does not persist anything in any store. Its only purpose is to trigger a notification to the `onOrderUpdated` subscription.

The API also uses [two authorizers](https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/appsync-construct.ts#L41-L47): `API_KEY` (for users to place orders) and `IAM` for the back end to call the `notifyOrderUpdated` mutation (this is a requirement from the [EventBridge integration](https://docs.aws.amazon.com/eventbridge/latest/userguide/target-appsync.html)).

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">In a real app, we'd probably use a Cognito User Pool, or OIDC, for user-facing authentication. I used an API key for simplicity.</div>
</div>

## The Step Functions State Machine

The second component of our application is a [Step Functions state machine](https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/state-machine-construct.ts). This state machine orchestrates the different steps of processing an order: payment, waiting for the restaurant to prepare the meal, waiting for delivery, etc.

For simplicity, I used [wait tasks](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-wait-state.html) to simulate the workflow. In a real application, the state machine would wait for real processes (e.g. payment gateway), or humans (e.g. meal preparation) before transitioning between the different states (hint: probably using the [callback pattern](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token)).

The most important part is the `EventBridgePutEvents` tasks. After every step in the order processing workflow, it receives the updated order and puts an event into the event bus with all its details. In the next section, we'll see how they are being handled.

```typescript
// lib/state-machine-construct.ts

createNotifyUpdate(name: string) {
  return new EventBridgePutEvents(this, `Notify ${name}`, {
    entries: [
      // generates an event from the task input
      {
        source: 'order.processing',
        detailType: 'order.updated',
        detail: TaskInput.fromObject({
          'order.$': '$.order',
        }),
        eventBus: this.eventBus,
      },
    ],
    resultPath: '$.eventBridgeResult',
  });
}
```

## Put It All Together

So far, we've learned about the AppSync API, and the Step Functions workflow. Did you notice that both those services place events into EventBridge? What I haven't shown you yet is how those events are used and how they play together.

In our application, the first thing that happens is a user placing an order. That's the starting point of the whole process, and it results in an `order.created` event. If you look at our [state machine construct](https://github.com/bboure/serverless-eda-graphql-api/blob/main/lib/state-machine-construct.ts), you'll notice that it subscribes to those events to start a new execution.

```typescript
new Rule(this, 'OrderHandlerRule', {
  eventBus: eventBus,
  eventPattern: {
    // subscribe to order.created events, coming from the api.
    source: ['order.api'],
    detailType: ['order.created'],
  },
  targets: [
    new SfnStateMachine(sm, {
      input: RuleTargetInput.fromObject({
        order: EventField.fromPath('$.detail'),
      }),
    }),
  ],
});
```

This means, that each time an order is placed, a Step Functions workflow will start with the order details as its input.

Then, as the state machine goes over all the different steps of the order processing, it emits `order.updated` events, which in turn are picked up by an EventBridge-AppSync rule.

```typescript
new CfnRule(scope, 'UpdateOrder', {
  eventBusName: eventBus.eventBusName,
  eventPattern: {
    source: ['order.processing'],
    'detail-type': ['order.updated'],
  },
  targets: [
    {
      id: 'OrderUpdated',
      arn: (api.node.defaultChild as CfnGraphQLApi).attrGraphQlEndpointArn,
      roleArn: ebRuleRole.roleArn,
      appSyncParameters: {
        graphQlOperation: `mutation NotifyOrderUpdated($input: NotifyOrderUpdatedInput!) { notifyOrderUpdated(input: $input) { id status updatedAt } }`,
      },
      inputTransformer: {
        inputPathsMap: {
          id: '$.detail.order.id',
          status: '$.detail.order.status',
          updatedAt: '$.detail.order.updatedAt',
        },
        inputTemplate: JSON.stringify({
          input: {
            id: '<id>',
            status: '<status>',
            updatedAt: '<updatedAt>',
          },
        }),
      },
    },
  ],
});
```

This is a direct integration between EventBridge and AWS AppSync. EventBridge invokes the `notifyOrderUpdated` mutation and uses the event's `details` attributes to build the input variables of the request. In turn, the mutation emits a message to all subscribers of the `onOrderUpdated` subscription.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">At the time of writing this article, there is no L2 construct for the EventBridge-AppSync integration, but we can use the L1 <code>CfnRule</code> construct.</div>
</div>

With all this in place, the front end can use the `onOrderUpdate` subscription to get real-time updates as soon as it places an order.

```graphql
subscription OnOrderUpdated($id: ID!) {
  onOrderUpdated(id: $id) {
    id
    status
    updatedAt
  }
}
```

Here is a simulation using GraphBolt. Look at the subscription messages coming in. Pay attention to the `status` field.

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1708501001608/1aea077d-a87a-4944-ab66-06bb8d43b9d7.gif align="center")

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text"><a target="_blank" rel="noopener noreferrer nofollow" href="https://graphbolt.dev?utm_campaign=eda-post&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_term=graphbolt" style="pointer-events: none">GraphBolt</a> is a desktop app to build, test, debug, and manage AWS AppSync APIs. <a target="_blank" rel="noopener noreferrer nofollow" href="https://graphbolt.dev?utm_campaign=eda-post&amp;utm_medium=Blog&amp;utm_source=blog.graphbolt.dev&amp;utm_term=try-it-for-free" style="pointer-events: none">Try it for free</a> today!</div>
</div>

# Conclusion

Leveraging event-driven architecture with AWS AppSync, Step Functions, and EventBridge offers a seamless solution for building efficient, asynchronous, and real-time applications. In this article, I explained with a practical use case how you can achieve it, and the best part is that we almost did not write any code for it (outside the infrastructure).

If you liked this content, please share! You can also follow me on [X](https://twitter.com/Benoit_Boure), [Hashnode](https://hashnode.com/@bboure), and [LinkedIn](https://www.linkedin.com/in/bboure/) for more, or subscribe to this blog's [newsletter](https://blog.graphbolt.dev/newsletter).

Thank you!
