Event-Driven Architecture on AWS
Build a real-time inventory tracker using EventBridge, Lambda, and DynamoDB.
Introduction
30 Second Summary
Think about what happens when a warehouse runs low on a popular product. Someone spots the shortage, fires off an alert, and the purchasing team kicks off a reorder.
In this project, you will build that same workflow using event-driven architecture on AWS. You will fire stock change events and watch them cascade through independent services that update inventory, detect low stock, and send email alerts.
What You'll Build
You will fire a stock change event from the terminal and watch EventBridge route it to a Lambda function that updates DynamoDB, detects low stock, and triggers an email alert in your inbox within seconds.
By the end of this project, you'll have:
- A working event-driven pipeline where you fire a stock change event from the terminal and watch DynamoDB update in real time.
- Event chaining in action, where one Lambda function detects low stock and fires a second event that triggers a completely separate Lambda.
- SNS email notifications that alert you within seconds when inventory drops below a reorder threshold.
- Secret Mission: Add an Amazon Lex V2 chatbot that lets you check stock levels by asking a question in plain English.
Do I need any prerequisites?
You will need an AWS account with the AWS CLI installed and configured on your machine. If you have completed other AWS projects on NextWork, you are ready to go.
Before We Start
Before you start creating AWS resources, take a moment to lock in what you're about to build and why it matters.
Set Up DynamoDB and SNS
You have committed to building an event-driven inventory tracker. Before any event-driven logic can work, you need two foundational pieces: a database to hold your inventory data, and a notification channel to alert you when stock runs low.
In this step, you will create an Amazon DynamoDB table to store product inventory and an Amazon SNS topic to deliver low-stock alerts to your email. These two services become the backbone that your Lambda functions will interact with in later steps.
In this step, get ready to:
- Create a DynamoDB table and seed it with sample product data.
- Create an SNS topic and subscribe your email for alerts.
- Verify your data and subscription are ready to go.
Create the Inventory table in DynamoDB
Every product in your inventory needs a home. DynamoDB is a fully managed NoSQL database that stores data in tables. You will create a table called Inventory with a partition key of ProductId so each product has a unique identifier.
- Create the DynamoDB table by running this command in your terminal:
aws dynamodb create-table \
--table-name Inventory \
--attribute-definitions AttributeName=ProductId,AttributeType=S \
--key-schema AttributeName=ProductId,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
What does this command do?
This creates a DynamoDB table called Inventory with ProductId as the partition key (the unique identifier for each item).
The --billing-mode PAY_PER_REQUEST flag sets the table to on-demand pricing. You only pay for the reads and writes you actually perform, which keeps costs at zero for a learning project.
You should see a JSON response with table details including "TableStatus": "CREATING". The table needs a few seconds to become active.
- Wait for the table to become active by running:
aws dynamodb wait table-exists --table-name Inventory
This command produces no output. When your terminal prompt returns, the table is ready.
Seeing an error about credentials?
Make sure your AWS CLI is configured with valid credentials. Run aws sts get-caller-identity to verify your CLI can connect to your AWS account.
If you see a region error, set your default region with aws configure and enter a region like us-east-1.
Need more help? Help me troubleshoot my AWS CLI configuration.
Seed the table with sample products
An empty table is not very useful. You need products to track. You will add three sample products, each with a name, a stock quantity, and a reorder threshold that tells the system when stock is dangerously low.
- Add the first product, Blue Widget, by running:
aws dynamodb put-item \
--table-name Inventory \
--item '{"ProductId": {"S": "blue-widget"}, "ProductName": {"S": "Blue Widget"}, "Quantity": {"N": "50"}, "ReorderThreshold": {"N": "5"}}'
What does the item structure mean?
DynamoDB requires you to specify each attribute's data type. {"S": "blue-widget"} means a String value, and {"N": "50"} means a Number value (passed as a string in the JSON).
The ReorderThreshold is the number at which the system should trigger a restock alert. Blue Widget has a threshold of 5, meaning you want to be alerted when stock drops to 5 or below.
- Add the second product, Red Gadget, by running:
aws dynamodb put-item \
--table-name Inventory \
--item '{"ProductId": {"S": "red-gadget"}, "ProductName": {"S": "Red Gadget"}, "Quantity": {"N": "30"}, "ReorderThreshold": {"N": "10"}}'
- Add the third product, Green Gizmo, by running:
aws dynamodb put-item \
--table-name Inventory \
--item '{"ProductId": {"S": "green-gizmo"}, "ProductName": {"S": "Green Gizmo"}, "Quantity": {"N": "20"}, "ReorderThreshold": {"N": "8"}}'
Each put-item command produces no output on success. That silence means the item was written.
Before you run the next command, what do you expect to see when you scan the table? How many items and which fields?
- Verify all three products are in the table by running:
aws dynamodb scan --table-name Inventory
You should see a JSON response with "Count": 3 and all three products listed with their ProductId, ProductName, Quantity, and ReorderThreshold values.
Seeing fewer than 3 items?
Double check that each put-item command ran without errors. If you see a JSON parsing error, make sure your quotes are correct. Single quotes wrap the entire --item value, and double quotes wrap each key and value inside.
On Windows, you may need to escape the inner double quotes differently. Try replacing the single-quote wrapper with double quotes and escaping inner quotes with backslashes.
Still stuck? Help me fix my DynamoDB put-item command.
Create an SNS topic and subscribe your email
Your inventory system needs a way to send alerts when stock runs low. Amazon SNS (Simple Notification Service) is a pub/sub messaging service. You create a topic (a named channel), subscribe your email to it, and any message published to that topic arrives in your inbox.
- Create the SNS topic by running:
aws sns create-topic --name InventoryAlerts
You should see a JSON response containing a TopicArn value that looks like arn:aws:sns:<region>:<account-id>:InventoryAlerts. Copy this ARN. You need it for the next command.
What is a TopicArn?
An ARN (Amazon Resource Name) is a unique identifier for any resource in AWS. The TopicArn uniquely identifies your InventoryAlerts topic across all of AWS. You will use it whenever you need to reference this topic.
- Subscribe your email address to the topic by running this command, replacing the topic ARN and email address with your own:
aws sns subscribe \
--topic-arn [[TOPIC_ARN="your InventoryAlerts topic ARN"]] \
--protocol email \
--notification-endpoint [[YOUR_EMAIL="your email address"]]
You should see a response with "SubscriptionArn": "pending confirmation". This means AWS sent a confirmation email to your address.
- Open your email inbox and look for an email from AWS Notifications.
- Click the Confirm subscription link in the email.
Check your spam folder
The confirmation email sometimes lands in your spam or junk folder. If you do not see it within a minute, check there. The confirmation link expires after two days.
Your browser opens a page that says Subscription confirmed!. Your email is now subscribed to the InventoryAlerts topic.
Never received the confirmation email?
Make sure you entered the correct email address in the --notification-endpoint flag. You can re-run the subscribe command with the correct email. SNS will send a new confirmation email.
Need help? Help me troubleshoot my SNS email subscription.
Your data layer and notification channel are ready. You have a DynamoDB table with three products and an SNS topic wired to your email. Next up, you will build a Lambda function that can actually update inventory quantities in this table.
Build and Test the StockUpdater Lambda
You have inventory data sitting in DynamoDB, but no way to process stock changes automatically. You need a function that can receive a stock change event, update the right product's quantity, and return the result.
In this step, you will create an IAM role that gives your function the permissions it needs, write the Lambda function code, deploy it, and test it by hand. Testing manually first lets you confirm the database update works before wiring up any event-driven automation.
In this step, get ready to:
- Create an IAM role with permissions for DynamoDB, EventBridge, and CloudWatch Logs.
- Write and deploy the StockUpdater Lambda function.
- Test the function manually and verify DynamoDB updates.
Create the Lambda execution role
Every Lambda function needs an execution role. This IAM role tells AWS which services the function is allowed to interact with.
- Create a trust policy file that allows Lambda to assume the role by running this command:
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
- Create the IAM role called LambdaInventoryRole by running:
aws iam create-role \
--role-name LambdaInventoryRole \
--assume-role-policy-document file://trust-policy.json
You should see JSON output that includes the role's ARN. It looks something like arn:aws:iam::123456789012:role/LambdaInventoryRole.
- Copy and save the Arn value from the output. You will need it when deploying the Lambda function: your role ARN
What is a trust policy?
A trust policy defines who is allowed to "assume" (use) this role. By listing lambda.amazonaws.com as the principal, you are telling AWS that the Lambda service can use this role's permissions when running your function.
Now the role exists, but it has no permissions yet. You need to attach a policy that grants access to DynamoDB, EventBridge, and CloudWatch Logs.
- Create a permissions policy file by running:
cat > permissions-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"dynamodb:UpdateItem",
"dynamodb:GetItem"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
EOF
- Attach the permissions policy to the role by running:
aws iam put-role-policy \
--role-name LambdaInventoryRole \
--policy-name LambdaInventoryPermissions \
--policy-document file://permissions-policy.json
What does this policy allow?
The policy grants three groups of permissions. dynamodb:UpdateItem and dynamodb:GetItem let the function read and write inventory data. events:PutEvents lets the function publish events to EventBridge (you will use this in a later step).
The CloudWatch Logs actions let the function write log output, which is how you will confirm the function ran.
Seeing an access denied error?
Make sure the trust-policy.json and permissions-policy.json files are in your current directory. The file:// prefix tells the CLI to read from a local file.
Also confirm your AWS CLI user has iam:CreateRole and iam:PutRolePolicy permissions. Help me troubleshoot my IAM role creation error.
Write the StockUpdater function
The StockUpdater function receives a stock change event, updates the product's quantity in DynamoDB, and returns the new quantity.
- Create a file called stock_updater.py and add the imports and setup code:
import json
import boto3
import os
# Connect to DynamoDB and EventBridge
dynamodb = boto3.resource("dynamodb")
events_client = boto3.client("events")
table = dynamodb.Table("Inventory")
EVENT_BUS_NAME = "InventoryEventBus"
What does this setup do?
The boto3 library is the AWS SDK for Python. It comes pre-installed in the Lambda runtime, so you do not need to package it separately.
The dynamodb.Table("Inventory") line connects to the DynamoDB table you created in Step 1. The events_client and EVENT_BUS_NAME are set up for a future step where the function will publish events.
- Below the setup code in stock_updater.py, add the handler function:
def lambda_handler(event, context):
# Extract product ID and quantity change from the event
detail = event.get("detail", {})
product_id = detail["ProductId"]
quantity_change = detail["QuantityChange"]
# Update the product's quantity in DynamoDB
response = table.update_item(
Key={"ProductId": product_id},
UpdateExpression="SET Quantity = Quantity + :change",
ExpressionAttributeValues={":change": quantity_change},
ReturnValues="ALL_NEW",
)
# Read back the updated attributes
updated_item = response["Attributes"]
new_quantity = int(updated_item["Quantity"])
reorder_threshold = int(updated_item["ReorderThreshold"])
product_name = updated_item.get("ProductName", product_id)
print(f"Updated {product_id}: new quantity = {new_quantity}")
return {
"statusCode": 200,
"body": json.dumps(
{"ProductId": product_id, "NewQuantity": new_quantity}
),
}
What does this handler do?
The handler pulls ProductId and QuantityChange from the incoming event. It then calls update_item with an expression that adds the change to the current quantity. A negative value decreases stock.
The ReturnValues="ALL_NEW" parameter tells DynamoDB to send back the full updated item. This lets the function read the new quantity and threshold without making a second call.
- Save stock_updater.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved stock_updater.py before continuing.
ⓧ I'd like to double check the full code
Here is the complete stock_updater.py file. Make sure your file matches this exactly:
import json
import boto3
import os
# Connect to DynamoDB and EventBridge
dynamodb = boto3.resource("dynamodb")
events_client = boto3.client("events")
table = dynamodb.Table("Inventory")
EVENT_BUS_NAME = "InventoryEventBus"
def lambda_handler(event, context):
# Extract product ID and quantity change from the event
detail = event.get("detail", {})
product_id = detail["ProductId"]
quantity_change = detail["QuantityChange"]
# Update the product's quantity in DynamoDB
response = table.update_item(
Key={"ProductId": product_id},
UpdateExpression="SET Quantity = Quantity + :change",
ExpressionAttributeValues={":change": quantity_change},
ReturnValues="ALL_NEW",
)
# Read back the updated attributes
updated_item = response["Attributes"]
new_quantity = int(updated_item["Quantity"])
reorder_threshold = int(updated_item["ReorderThreshold"])
product_name = updated_item.get("ProductName", product_id)
print(f"Updated {product_id}: new quantity = {new_quantity}")
return {
"statusCode": 200,
"body": json.dumps(
{"ProductId": product_id, "NewQuantity": new_quantity}
),
}
Unsure where to create the file?
Create stock_updater.py in the same directory where you created the trust and permissions policy JSON files. You can use any text editor. Help me create the stock_updater.py file.
Deploy and test the function
Your code is ready. Now you will zip it up, deploy it to Lambda, and invoke it with a test event to see the database change in real time.
- Package the function code into a zip file by running:
zip stock_updater.zip stock_updater.py
- Deploy the function to Lambda by running this command. Replace the --role value with the ARN you saved earlier:
aws lambda create-function \
--function-name StockUpdater \
--runtime python3.13 \
--zip-file fileb://stock_updater.zip \
--handler stock_updater.lambda_handler \
--role [[ROLE_ARN="your role ARN"]]
You should see JSON output with details about your new function, including "Runtime": "python3.13" and "Handler": "stock_updater.lambda_handler".
What does this command do?
The --zip-file fileb:// flag uploads your local zip file directly. The --handler value tells Lambda which file and function to call: stock_updater is the filename (without .py) and lambda_handler is the function name inside it.
Seeing a role-not-found error?
IAM role creation can take a few seconds to propagate. Wait about 10 seconds and try the command again. Also double check that the --role value is the full ARN starting with arn:aws:iam::. Help me fix my Lambda deployment error.
Time to test. You will invoke the function with a simulated stock change event that decreases Blue Widget's quantity by 5.
- Create a test event file by running:
cat > test-event.json << 'EOF'
{
"detail": {
"ProductId": "blue-widget",
"QuantityChange": -5
}
}
EOF
- Before you run the next command, think about it: Blue Widget currently has 50 units. You are sending a QuantityChange of -5. What do you expect the new quantity to be?
- Invoke the function with the test event by running:
aws lambda invoke \
--function-name StockUpdater \
--cli-binary-format raw-in-base64-out \
--payload file://test-event.json \
response.json
The terminal shows "StatusCode": 200. That means Lambda ran the function successfully.
- Check what the function returned by running:
cat response.json
You should see {"statusCode": 200, "body": "{\"ProductId\": \"blue-widget\", \"NewQuantity\": 45}"}. The quantity dropped from 50 to 45.
Now verify the change landed in DynamoDB.
- Query the updated item by running:
aws dynamodb get-item \
--table-name Inventory \
--key '{"ProductId": {"S": "blue-widget"}}'
You should see the Blue Widget item with "Quantity" now showing 45 instead of the original 50. Your Lambda function successfully updated DynamoDB.
Quantity still showing 50?
Check the response.json file for error details. If you see a FunctionError in the invoke output, the function code has an issue. Common causes include a typo in the UpdateExpression or a mismatch between ProductId in the test event and the table.
Help me debug why my StockUpdater Lambda didn't update DynamoDB.
Your StockUpdater Lambda works. Right now you have to invoke it by hand every time. In the next step, you will connect it to EventBridge so that any system can fire a stock change event and the function reacts automatically.
Wire Up EventBridge
In the last step you proved the StockUpdater Lambda can update DynamoDB when you invoke it manually. That is useful for testing, but it defeats the purpose of an event-driven system.
The whole point of event-driven architecture is that producers and consumers never call each other directly. A producer fires an event onto a bus, and the bus routes it to whatever consumers are listening. In this step you will create a custom EventBridge event bus, attach a routing rule, and fire a real event from the CLI so the Lambda reacts automatically.
In this step, get ready to:
- Create a custom EventBridge event bus.
- Create a routing rule that matches stock change events and targets your Lambda.
- Fire a test event from the CLI and watch DynamoDB update automatically.
Create the custom event bus
AWS provides a default event bus for every account, but best practice is to create a dedicated bus for your application. This keeps your inventory events separate from AWS service events and makes routing rules simpler.
- Create a custom event bus called InventoryEventBus by running this command:
aws events create-event-bus --name InventoryEventBus
You should see a JSON response containing the event bus ARN.
What is a custom event bus?
Think of an event bus as a highway for messages. The default bus carries AWS service events (like EC2 state changes). Your custom bus carries only the events your application publishes. Rules on the bus act like exit ramps, directing specific events to the right target.
- Confirm the bus exists by running:
aws events describe-event-bus --name InventoryEventBus
You should see the bus name and ARN in the output.
Don't see the event bus?
Make sure you typed the name exactly as InventoryEventBus with matching capitalisation. EventBridge bus names are case-sensitive.
Help me troubleshoot why my EventBridge custom event bus was not created.
Create the routing rule and add the Lambda target
A rule tells EventBridge which events to catch and where to send them. You will create a rule that matches events with source inventory.system and detail-type StockChanged, then point it at the StockUpdater Lambda.
- Create the StockChangedRule on your custom bus by running:
aws events put-rule \
--name StockChangedRule \
--event-bus-name InventoryEventBus \
--event-pattern '{"source":["inventory.system"],"detail-type":["StockChanged"]}'
What does this event pattern do?
The --event-pattern flag defines a filter. EventBridge inspects every event that arrives on InventoryEventBus and only routes events whose source is inventory.system and whose detail-type is StockChanged.
The command returns a RuleArn in the output. Copy this value. You will need it in the next command.
- Save the rule ARN that was returned. It looks like arn:aws:events:REGION:ACCOUNT_ID:rule/InventoryEventBus/StockChangedRule.
- Retrieve your StockUpdater Lambda function ARN by running:
aws lambda get-function --function-name StockUpdater --query 'Configuration.FunctionArn' --output text
- Copy the ARN that is printed. It looks like arn:aws:lambda:REGION:ACCOUNT_ID:function:StockUpdater.
- Add the StockUpdater Lambda as the target of your rule by running:
aws events put-targets \
--rule StockChangedRule \
--event-bus-name InventoryEventBus \
--targets "Id"="StockUpdaterTarget","Arn"="[[LAMBDA_ARN="your StockUpdater Lambda ARN"]]"
What does this command do?
The put-targets command wires a destination to the rule. Every event that matches StockChangedRule will now be forwarded to the StockUpdater Lambda. The Id is a label you choose to identify this particular target within the rule.
You should see FailedEntryCount as 0 in the response.
There is one more permission step. EventBridge needs explicit permission to invoke your Lambda function.
- Grant EventBridge permission to invoke StockUpdater by running:
aws lambda add-permission \
--function-name StockUpdater \
--statement-id EventBridgeInvokeStockUpdater \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn "[[RULE_ARN="your StockChangedRule ARN from the put-rule output"]]"
Why is this permission needed?
The IAM role you created earlier lets the Lambda talk to DynamoDB and EventBridge. But EventBridge also needs permission in the other direction. This add-permission command adds a resource-based policy on the Lambda itself, telling it to accept invocations from EventBridge when the request comes from your specific rule.
You should see a JSON response containing the new policy statement.
Seeing a 'ResourceConflictException'?
This means a permission with the same --statement-id already exists. Each statement ID must be unique. If you already ran the command once, it worked the first time. You can safely move on.
Help me troubleshoot my Lambda add-permission error for EventBridge.
Fire a test event and verify the chain
Everything is wired up. Time to prove it works end-to-end by firing a StockChanged event from the CLI and watching DynamoDB update without manually invoking the Lambda.
- Before you run the test, check the current quantity for Red Gadget:
aws dynamodb get-item \
--table-name Inventory \
--key '{"ProductId":{"S":"red-gadget"}}' \
--query 'Item.Quantity.N' \
--output text
- Note the number that is returned. You will compare it after the event fires.
Before you run the next command, what do you think will happen? Will DynamoDB update automatically, or will it stay the same?
- Fire a stock change event to reduce Red Gadget by 3 by running:
aws events put-events \
--entries '[{"Source":"inventory.system","DetailType":"StockChanged","Detail":"{\"ProductId\":\"red-gadget\",\"QuantityChange\":-3}","EventBusName":"InventoryEventBus"}]'
What does this command do?
The put-events command publishes a custom event to InventoryEventBus. The event has Source set to inventory.system and DetailType set to StockChanged, matching your rule's pattern. The Detail field carries the payload with ProductId and QuantityChange.
EventBridge receives the event, matches it to StockChangedRule, and invokes StockUpdater automatically.
You should see FailedEntryCount as 0 in the response. Now wait about 5 seconds for EventBridge to route the event and the Lambda to execute.
- Verify the Lambda ran by checking CloudWatch Logs:
aws logs filter-log-events \
--log-group-name /aws/lambda/StockUpdater \
--filter-pattern "Updated red-gadget" \
--limit 1
You should see a log entry containing a message like Updated red-gadget: new quantity = followed by the new value.
- Confirm DynamoDB was updated by running:
aws dynamodb get-item \
--table-name Inventory \
--key '{"ProductId":{"S":"red-gadget"}}' \
--query 'Item.Quantity.N' \
--output text
The quantity should be 3 less than the number you noted earlier. You never called the Lambda directly. EventBridge handled the routing automatically.
DynamoDB quantity didn't change?
Make sure the EventBusName in your put-events command is set to InventoryEventBus. If you omit it, the event goes to the default bus where your rule does not exist.
Check that Source is exactly inventory.system and DetailType is exactly StockChanged. The event pattern match is case-sensitive.
Verify you ran the add-permission command. Without it, EventBridge cannot invoke the Lambda even though the rule matches.
Help me figure out why my EventBridge event did not trigger my Lambda function.
Your event-driven wiring is live. A stock change event fired from the CLI cascades through EventBridge to Lambda to DynamoDB without any direct calls between services. Next up, you will add threshold detection so the Lambda can publish its own events, creating a true event chain.
Add Event Chaining with Threshold Detection
Your StockUpdater Lambda can update quantities in DynamoDB, but it has zero awareness of whether stock levels are dangerously low. A warehouse system that silently runs out of inventory is a problem waiting to happen.
In this step, you'll add threshold detection to the StockUpdater Lambda. When the new quantity drops at or below a product's reorder threshold, the Lambda publishes a second event back to EventBridge. This is event chaining: one event triggers a function that conditionally fires another event, cascading reactions across independent services.
In this step, get ready to:
- Update the StockUpdater Lambda with threshold detection and ReorderRequired event publishing.
- Fire a stock change event that drops Blue Widget below its reorder threshold.
- Observe that the ReorderRequired event fires but nothing catches it yet.
Add threshold detection to the StockUpdater Lambda
Right now, the StockUpdater Lambda updates the quantity and returns. It never checks whether the new level is safe or critically low. You need it to compare the updated quantity against the product's ReorderThreshold and, if stock is too low, publish a new event.
- Open stock_updater.py in your text editor.
- Find this line near the bottom of the lambda_handler function:
print(f"Updated {product_id}: new quantity = {new_quantity}")
- Add the following threshold detection block directly below that print line, before the return statement:
if new_quantity <= reorder_threshold:
print(f"Publishing ReorderRequired event for {product_id}")
events_client.put_events(
Entries=[
{
"Source": "inventory.system",
"DetailType": "ReorderRequired",
"Detail": json.dumps(
{
"ProductId": product_id,
"ProductName": product_name,
"CurrentQuantity": new_quantity,
"ReorderThreshold": reorder_threshold,
}
),
"EventBusName": EVENT_BUS_NAME,
}
]
)
What does this code do?
This block turns your Lambda from a simple updater into an event producer.
- if new_quantity <= reorder_threshold compares the freshly updated quantity against the product's stored threshold. If stock is at or below the safe level, the block fires.
- events_client.put_events() publishes a brand-new ReorderRequired event to the same InventoryEventBus. The event payload includes the product ID, name, current quantity, and threshold so any downstream consumer has full context.
- This is the core of event chaining: the Lambda is both a consumer of StockChanged events and a producer of ReorderRequired events.
- Save stock_updater.py.
- Zip and redeploy the updated function to Lambda by running these commands:
zip stock_updater.zip stock_updater.py
aws lambda update-function-code --function-name StockUpdater --zip-file fileb://stock_updater.zip
You should see JSON output with the function's FunctionName, a fresh LastModified timestamp, and an updated CodeSha256 hash confirming the new code is live.
Seeing a "function not found" error?
- Make sure the --function-name is exactly StockUpdater (case-sensitive).
- Make sure your terminal is in the same directory as stock_updater.py. Run ls to confirm the file is listed.
- Check that your AWS CLI region matches the region where you created the Lambda function.
Why can't I deploy my updated Lambda function?
✔️ Awesome, I've got everything!
Great. Double check you saved stock_updater.py and that the deploy command returned a fresh LastModified timestamp.
ⓧ I'd like to double check the full code
Your stock_updater.py is now over 30 lines, so here is the structure to verify. Scroll up and confirm each section is present in order:
- Imports and client setup at the top (import json, import boto3, import os, DynamoDB resource, events client, table reference, and EVENT_BUS_NAME).
- lambda_handler function with table.update_item() that updates the quantity and reads back new_quantity, reorder_threshold, and product_name.
- The print(f"Updated ...") line followed by the new if new_quantity <= reorder_threshold: block that publishes a ReorderRequired event.
- The return statement at the bottom returning statusCode and the updated product info.
Fire a low-stock event and verify threshold detection
The updated Lambda is live. Time to trigger it with a stock change large enough to push Blue Widget below its reorder threshold of 5.
- First, check Blue Widget's current quantity by running:
aws dynamodb get-item --table-name Inventory --key '{"ProductId": {"S": "blue-widget"}}'
Note the current Quantity value. You are about to send a large negative change to guarantee the quantity drops well below the threshold.
- Fire a stock change event that subtracts 100 units from Blue Widget by running:
aws events put-events --entries '[{"Source":"inventory.system","DetailType":"StockChanged","Detail":"{\"ProductId\":\"blue-widget\",\"QuantityChange\":-100}","EventBusName":"InventoryEventBus"}]'
Why subtract 100?
Blue Widget's reorder threshold is 5. Your earlier tests may have changed its quantity, so a large subtraction guarantees the new quantity lands below the threshold regardless of the current value. A negative inventory is perfectly fine for this test.
You should see a response with "FailedEntryCount": 0 confirming the event was accepted.
- Wait about 10 seconds for the Lambda to execute, then check the CloudWatch Logs by running:
aws logs tail /aws/lambda/StockUpdater --since 2m
Look for two log lines in the output. First, Updated blue-widget: new quantity = followed by a negative number. Second, Publishing ReorderRequired event for blue-widget. That second line proves event chaining is working.
- Confirm the DynamoDB update by running:
aws dynamodb get-item --table-name Inventory --key '{"ProductId": {"S": "blue-widget"}}'
The Quantity should now be a negative number, well below the ReorderThreshold of 5.
Not seeing the ReorderRequired log line?
- Make sure you saved stock_updater.py before zipping and redeploying. The zip packages whatever file is on disk, not what is open in your editor.
- Confirm the if block is indented at the same level as the print statement above it (inside lambda_handler). Incorrect indentation means the block is outside the function.
- If you see the "Updated" log but not the "Publishing" log, the quantity may not have dropped below the threshold. Run the get-item command to check the current quantity and threshold values.
Why isn't my threshold detection firing in CloudWatch Logs?
Observe the gap in your event chain
Your CloudWatch Logs confirm that the StockUpdater Lambda published a ReorderRequired event to InventoryEventBus. Before you move on, do you think a low-stock notification arrived in your email inbox?
- Check your email inbox now.
No alert. The ReorderRequired event was published to EventBridge, but no rule exists to catch events with detail-type ReorderRequired. The event fired into the void.
Why is this gap important?
This is the power of decoupled architecture in action. The StockUpdater Lambda does not care what happens to the ReorderRequired event after it publishes it. It does its job (update stock, publish the event) and moves on.
The gap is intentional. You can add a new consumer for ReorderRequired events without modifying the StockUpdater at all. That is exactly what you will do in the next step.
Your event chain is half-built. The StockUpdater detects low stock and fires a reorder event, but the event vanishes with no listener. Next up, you'll close that gap by building a second Lambda that catches ReorderRequired events and sends a real alert to your inbox via SNS.
Build the ReorderProcessor and Close the Chain
In Step 4, you proved that the StockUpdater Lambda can detect low stock and fire a ReorderRequired event. You confirmed it in CloudWatch Logs. But that event is flying into the void.
In this step, you will build a second Lambda function that catches the ReorderRequired event and sends an SNS email alert. This closes the event chain: one event triggers a Lambda that fires a second event, which triggers a second Lambda.
In this step, get ready to:
- Write the ReorderProcessor Lambda function.
- Deploy the function and create the EventBridge rule.
- Test the full event chain end-to-end.
Write the ReorderProcessor Lambda function
The ReorderProcessor reads incoming ReorderRequired events and publishes an alert to the InventoryAlerts SNS topic. It is completely independent of the StockUpdater. It only knows about the event it receives and the topic it publishes to.
- Create a new file called reorder_processor.py in the same folder where you created stock_updater.py.
- Add the imports, SNS client, and main handler by pasting the following code:
import json
import boto3
sns_client = boto3.client("sns")
def lambda_handler(event, context):
# Extract product details from the ReorderRequired event
detail = event.get("detail", {})
product_id = detail["ProductId"]
product_name = detail.get("ProductName", product_id)
current_quantity = detail["CurrentQuantity"]
reorder_threshold = detail["ReorderThreshold"]
# Look up the SNS topic and build the alert message
topic_arn = get_topic_arn("InventoryAlerts")
message = (
f"Low Stock Alert: {product_name} is at {current_quantity} units "
f"(threshold: {reorder_threshold}). Reorder needed!"
)
# Publish the alert to SNS
sns_client.publish(
TopicArn=topic_arn,
Subject=f"Low Stock Alert: {product_name}",
Message=message,
)
print(f"Sent reorder alert for {product_id}")
return {"statusCode": 200, "body": "Reorder alert sent"}
What does this code do?
- It pulls ProductId, ProductName, CurrentQuantity, and ReorderThreshold from the ReorderRequired event detail.
- It dynamically looks up the InventoryAlerts topic ARN so you never need to hard-code account-specific ARNs.
- It builds a human-readable alert message and publishes it to SNS, which delivers the email to your subscribed address.
- Below the lambda_handler function, add the helper that looks up the SNS topic ARN:
def get_topic_arn(topic_name):
# List all SNS topics and find the one matching our topic name
response = sns_client.list_topics()
for topic in response.get("Topics", []):
if topic["TopicArn"].endswith(f":{topic_name}"):
return topic["TopicArn"]
raise ValueError(f"SNS topic {topic_name} not found")
Why look up the topic ARN dynamically?
SNS topic ARNs include your account ID and region. Hard-coding them would make the code break if you deploy to a different account or region. get_topic_arn scans your topics at runtime and matches by name, keeping the function portable.
- Save reorder_processor.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your file has both blocks in order: the lambda_handler function (first block) and the get_topic_arn helper (second block). Make sure the helper sits outside lambda_handler, at the top level of the file.
Deploy the function and wire up EventBridge
Before you deploy, the LambdaInventoryRole needs permission to publish to SNS. Then you can package and deploy the function, and create the EventBridge rule that routes ReorderRequired events to it.
- Add SNS publish permissions to the LambdaInventoryRole by running:
aws iam put-role-policy \
--role-name LambdaInventoryRole \
--policy-name SNSPublishPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["sns:Publish", "sns:ListTopics"],
"Resource": "*"
}
]
}'
What does this command do?
This adds an inline policy called SNSPublishPolicy to the existing LambdaInventoryRole. It grants sns:Publish (to send alerts) and sns:ListTopics (to look up the topic ARN dynamically). The command produces no output on success.
- Package the Lambda code into a zip file by running:
zip reorder_processor.zip reorder_processor.py
- Deploy the ReorderProcessor Lambda function by running:
aws lambda create-function \
--function-name ReorderProcessor \
--runtime python3.13 \
--role arn:aws:iam::[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:role/LambdaInventoryRole \
--handler reorder_processor.lambda_handler \
--zip-file fileb://reorder_processor.zip
- You should see a JSON response with "FunctionName": "ReorderProcessor" and "Runtime": "python3.13". This confirms the function is deployed.
Seeing an error about the role?
- Make sure you replaced the account ID placeholder with your actual 12-digit AWS account ID.
- Confirm the role exists by running aws iam get-role --role-name LambdaInventoryRole.
- If you see a "function already exists" error, you may have already created it. Run aws lambda get-function --function-name ReorderProcessor to check.
My Lambda deployment failed. What should I check?
Now create the EventBridge rule that catches ReorderRequired events and routes them to the new function.
- Create the ReorderRequiredRule on the InventoryEventBus by running:
aws events put-rule \
--name ReorderRequiredRule \
--event-bus-name InventoryEventBus \
--event-pattern '{"source": ["inventory.system"], "detail-type": ["ReorderRequired"]}'
- You should see a JSON response with a RuleArn value. Copy the full ARN. You will need it in a moment.
- Add the ReorderProcessor Lambda as the target for this rule by running:
aws events put-targets \
--rule ReorderRequiredRule \
--event-bus-name InventoryEventBus \
--targets "Id"="ReorderProcessorTarget","Arn"="arn:aws:lambda:[[AWS_REGION="your AWS region, e.g. us-east-1"]]:[[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]]:function:ReorderProcessor"
- Grant EventBridge permission to invoke the ReorderProcessor function by running:
aws lambda add-permission \
--function-name ReorderProcessor \
--statement-id EventBridgeInvokeReorderProcessor \
--action lambda:InvokeFunction \
--principal events.amazonaws.com
What did these three commands do?
- The put-rule command created a rule that matches events with source inventory.system and detail-type ReorderRequired.
- The put-targets command told EventBridge to send matching events to the ReorderProcessor Lambda.
- The add-permission command granted EventBridge the right to invoke the function. Without this, EventBridge would have permission to route the event but not to trigger the Lambda.
Seeing a permissions error on put-targets?
- Make sure you replaced both the region and account ID placeholders in the Lambda ARN.
- Verify the function exists by running aws lambda get-function --function-name ReorderProcessor.
My EventBridge target setup isn't working. Help me debug the put-targets command.
Fire a stock change and watch the full chain
Everything is wired. Time to fire an event from the CLI and watch the full chain execute: EventBridge routes the StockChanged event to StockUpdater, StockUpdater updates DynamoDB and fires ReorderRequired, EventBridge routes the ReorderRequired event to ReorderProcessor, and ReorderProcessor sends you an email.
First, you need to make sure a product's quantity is above its reorder threshold so you can drop it below.
- Reset Blue Widget's quantity to 10 by running:
aws dynamodb update-item \
--table-name Inventory \
--key '{"ProductId": {"S": "blue-widget"}}' \
--update-expression "SET Quantity = :q" \
--expression-attribute-values '{":q": {"N": "10"}}'
- Verify the reset by running:
aws dynamodb get-item \
--table-name Inventory \
--key '{"ProductId": {"S": "blue-widget"}}' \
--query "Item.Quantity.N" \
--output text
- You should see 10 in the output. Blue Widget is safely above its reorder threshold of 5.
Before you fire the event, think about what you expect to happen. You are about to send a StockChanged event that subtracts 8 from Blue Widget's quantity of 10, dropping it to 2.
What do you think will happen next? Will you receive an email? Why or why not?
- Fire the stock change event by running:
aws events put-events --entries '[{
"Source": "inventory.system",
"DetailType": "StockChanged",
"Detail": "{\"ProductId\": \"blue-widget\", \"QuantityChange\": -8}",
"EventBusName": "InventoryEventBus"
}]'
- You should see a response with "FailedEntryCount": 0. The event was accepted by EventBridge.
Now the chain is executing. The quantity drops from 10 to 2, which is below the reorder threshold of 5. StockUpdater fires a ReorderRequired event. ReorderProcessor catches it and publishes to SNS.
- Check your email inbox. Within a few seconds, you should receive an email with the subject line containing Low Stock Alert and a message like: "Low Stock Alert: Blue Widget is at 2 units (threshold: 5). Reorder needed!"
What just happened?
You just witnessed event chaining in action. One CLI command fired a single StockChanged event. That event cascaded through four independent services without any of them knowing about each other directly.
- EventBridge routed StockChanged to StockUpdater.
- StockUpdater updated DynamoDB and published ReorderRequired.
- EventBridge routed ReorderRequired to ReorderProcessor.
- ReorderProcessor published an SNS alert to your inbox.
This is the core benefit of event-driven architecture: you added ReorderProcessor without modifying a single line of StockUpdater code.
No email in your inbox?
- Check your spam or junk folder. SNS emails sometimes land there.
- Confirm the SNS subscription is confirmed by running aws sns list-subscriptions and checking the status.
- Check CloudWatch Logs for the ReorderProcessor function. Look for log groups starting with /aws/lambda/ReorderProcessor. If no log group exists, the function was never invoked, which points to the EventBridge rule or permission setup.
- Check CloudWatch Logs for StockUpdater. If you do not see a "Publishing ReorderRequired event" log line, the quantity may not have dropped below the threshold. Run the get-item command above to check.
I didn't get the alert email. Help me trace the event chain.
Secret mission
Add a Lex V2 Inventory Chatbot
You have a powerful event-driven backend, but querying it still requires CLI commands. In this mission, you will add an Amazon Lex V2 chatbot called InventoryBot that lets you ask "What's the stock level for Blue Widget?" and get a real-time answer from DynamoDB.
Clean Up Your Resources
Clean Up Your Resources
All the resources in this project are serverless. They only incur costs when actively used. At the volumes of a learning project, everything stays well within the AWS free tier.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- DynamoDB table: Inventory.
- EventBridge custom event bus: InventoryEventBus.
- EventBridge rules: StockChangedRule and ReorderRequiredRule.
- Lambda functions: StockUpdater, ReorderProcessor, and InventoryBotFulfillment.
- IAM role: LambdaInventoryRole.
- SNS topic: InventoryAlerts with email subscription.
- Amazon Lex V2 bot: InventoryBot.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your event-driven system.
- All resources are serverless and only incur cost when invoked.
- DynamoDB on-demand billing means you pay only for reads and writes. Storage stays within the 25 GB free tier for a learning project.
- Lambda free tier covers 1 million invocations per month.
- SNS free tier covers 1,000 email notifications per month.
- Lex V2 free tier covers 10,000 text requests per month for 12 months.
Pause - I'll come back to this later
No action needed. Serverless resources do not incur charges when idle.
- Lambda functions, EventBridge rules, and SNS topics have zero idle cost.
- DynamoDB storage cost is negligible for three small items.
- When you return, everything will work exactly as you left it.
Delete - I don't want to use this again
Remove all project resources to return your AWS account to a clean state. Run each command below in your terminal.
- Delete the Lex V2 bot. First, find your bot ID in the Lex V2 console, then run:
- Open the Amazon Lex V2 console.
- Select InventoryBot, then click Delete. Confirm deletion.
- Delete the InventoryBotFulfillment Lambda function by running:
aws lambda delete-function --function-name InventoryBotFulfillment
- Remove targets from EventBridge rules, then delete the rules. You need to remove targets before deleting each rule. Run these commands:
aws events remove-targets --rule ReorderRequiredRule --event-bus-name InventoryEventBus --ids "1"
aws events delete-rule --name ReorderRequiredRule --event-bus-name InventoryEventBus
aws events remove-targets --rule StockChangedRule --event-bus-name InventoryEventBus --ids "1"
aws events delete-rule --name StockChangedRule --event-bus-name InventoryEventBus
- Delete the custom event bus by running:
aws events delete-event-bus --name InventoryEventBus
- Delete the ReorderProcessor Lambda function by running:
aws lambda delete-function --function-name ReorderProcessor
- Delete the StockUpdater Lambda function by running:
aws lambda delete-function --function-name StockUpdater
- Delete the SNS topic. Deleting the topic also removes all subscriptions. First find the topic ARN, then delete it:
aws sns list-topics
Find the ARN for InventoryAlerts in the output, then run:
aws sns delete-topic --topic-arn [[SNS_TOPIC_ARN="your InventoryAlerts topic ARN from the list above"]]
- Delete the DynamoDB table by running:
aws dynamodb delete-table --table-name Inventory
- Delete the IAM role. You must detach all policies before deleting the role. List the attached policies first, then detach each one:
aws iam list-attached-role-policies --role-name LambdaInventoryRole
For each policy ARN in the output, run:
aws iam detach-role-policy --role-name LambdaInventoryRole --policy-arn [[POLICY_ARN="the policy ARN from the list above"]]
Also list and delete any inline policies:
aws iam list-role-policies --role-name LambdaInventoryRole
For each inline policy name in the output, run:
aws iam delete-role-policy --role-name LambdaInventoryRole --policy-name [[INLINE_POLICY_NAME="the inline policy name from the list above"]]
Finally, delete the role itself:
aws iam delete-role --role-name LambdaInventoryRole
- Delete any local .py files you created for the Lambda functions (stock_updater.py, reorder_processor.py, inventory_bot_fulfillment.py, and any .zip files).
Nice Work!
Nice Work!
You just built a fully decoupled, event-driven inventory tracking system on AWS. From a single CLI event, you triggered an automated chain of reactions across independent serverless services.
You've learned how to:
- Route events through a custom EventBridge event bus using pattern-matching rules that automatically trigger Lambda functions to update a DynamoDB-backed inventory system.
- Implemented event chaining where the StockUpdater Lambda conditionally publishes a new event that triggers a second independent Lambda, proving that decoupled services can react to each other without ever being directly connected.
- Wired up SNS email notifications that alert you within seconds when inventory drops below a reorder threshold, demonstrating the core benefit of event-driven architecture: adding a new consumer without modifying the producer.
- Secret Mission: Added an Amazon Lex V2 chatbot that queries the inventory system through natural language, turning CLI commands into a conversational interface.
Ready to quiz yourself?