Building a Production-Style Serverless Order Management System on AWS
When I started this project, my goal was not just to build another basic CRUD API.
I wanted to understand how a real cloud backend is designed when we care about scalability, security, monitoring, cost, and maintainability.
So I built a Serverless Order Management System on AWS using:
Amazon API Gateway
AWS Lambda
Amazon DynamoDB
Amazon Cognito
IAM
Amazon CloudWatch
Amazon SNS
API Gateway throttling
Postman for API testing
GitHub Repository: Serverless Order Management System
Portfolio: kamleshcloud.com
LinkedIn: Kamlesh Dubale
What I Built
This project is a serverless backend for managing orders.
It supports:
Creating an order
Getting all orders
Getting a specific order by ID
Deleting an order
Protecting API routes using Cognito authentication
Monitoring Lambda errors with CloudWatch
Sending alerts using SNS
Restricting Lambda permissions using least-privilege IAM
Protecting the API from excessive traffic using throttling
The final architecture is fully serverless, meaning there are no EC2 instances or servers to manage.
High-Level Architecture
The request flow looks like this:
User / Postman
↓
Amazon Cognito
↓
Amazon API Gateway
↓
AWS Lambda
↓
Amazon DynamoDB
↓
CloudWatch Logs / Alarms
↓
SNS Email Alerts
API Gateway acts as the public entry point.
Lambda handles the backend logic.
DynamoDB stores the order records.
Cognito adds authentication.
CloudWatch and SNS provide monitoring and alerting.
IAM controls what Lambda is allowed to access.
API throttling protects the system from excessive traffic.
Why I Chose Serverless
Traditional backend systems usually require servers.
That means someone has to think about:
Operating system updates
Server patching
Scaling servers
Load balancing
Capacity planning
Availability
Runtime maintenance
With serverless, AWS handles most of that.
Lambda only runs when a request comes in. DynamoDB scales automatically. API Gateway gives a managed HTTPS endpoint. Cognito manages authentication. CloudWatch collects logs and metrics.
This makes serverless a very good fit for event-driven APIs, small backend services, prototypes, internal tools, and scalable cloud-native applications.
API Gateway Routes
I created four API routes:
| Method | Route | Purpose |
|---|---|---|
| POST | /orders |
Create a new order |
| GET | /orders |
List all orders |
| GET | /orders/{id} |
Get one order by ID |
| DELETE | /orders/{id} |
Delete one order |
This route structure follows a simple REST-style API design.
For example:
POST /orders
means create an order.
GET /orders
means list all orders.
GET /orders/{id}
means retrieve one specific order.
DELETE /orders/{id}
means delete one specific order.
Lambda Functions
Instead of using one large Lambda function for everything, I created separate Lambda functions:
Lambda Function | Responsibility |
CreateOrderFunction | Creates a new order |
GetOrderFunction | Gets one order |
ListOrdersFunction | Lists all orders |
DeleteOrderFunction | Deletes an order |
This follows the single responsibility principle.
Each function has one clear job.
This makes the system easier to debug, easier to secure, and easier to explain.
DynamoDB Table Design
The DynamoDB table is called:
orders
The partition key is:
OrderId
Each order item contains:
{
"OrderId": "994dd3bc-a43a-4342-b07b-3f5829e3c046",
"customerName": "Kamlesh",
"product": "MacBook Pro",
"quantity": 1
}
I used OrderId as the partition key because every order needs a unique identifier.
This allows DynamoDB to retrieve a specific order efficiently using GetItem.
Create Order Flow
When a client sends this request:
POST /orders
with this JSON body:
{
"customerName": "Kamlesh",
"product": "MacBook Pro",
"quantity": 1
}
the flow is:
Postman
↓
API Gateway
↓
CreateOrderFunction
↓
DynamoDB PutItem
↓
Order saved
The Lambda function generates a unique OrderId using Python’s uuid module.
Then it stores the item in DynamoDB using boto3.
Input Validation
A backend API should not blindly accept bad input.
So I added validation for required fields:
customerName
product
quantity
If the request body is missing required fields, the API returns a proper error response instead of crashing.
Example bad request:
{
"customerName": "Kamlesh"
}
Response:
{
"message": "Missing required fields",
"missingFields": ["product", "quantity"],
"requiredFields": ["customerName", "product", "quantity"]
}
This is important because production APIs should give clear and useful error messages.
Get All Orders
The GET /orders endpoint lists all orders from DynamoDB.
The response looked like this:
[
{
"product": "MacBook Pro",
"quantity": 1,
"customerName": "Kamlesh",
"OrderId": "994dd3bc-a43a-4342-b07b-3f5829e3c046"
}
]
For this project, I used DynamoDB Scan.
In a real high-scale production system, I would avoid unnecessary scans and design access patterns carefully using partition keys, sort keys, and indexes.
Get Order by ID
The GET /orders/{id} endpoint uses a path parameter.
Example:
GET /orders/994dd3bc-a43a-4342-b07b-3f5829e3c046
API Gateway passes the ID to Lambda inside:
event["pathParameters"]["id"]
Lambda then uses DynamoDB GetItem to retrieve the order.
This helped me understand how API Gateway converts HTTP requests into Lambda event objects.
Delete Order
The DELETE /orders/{id} endpoint deletes an order from DynamoDB.
The flow is:
Client
↓
API Gateway
↓
DeleteOrderFunction
↓
DynamoDB DeleteItem
↓
Deletion confirmation
This completed the main CRUD functionality of the project.
Cognito Authentication
After the CRUD APIs were working, I added Amazon Cognito.
The goal was to protect sensitive API routes.
Without authentication, anyone who knows the API URL can send requests.
That is not acceptable for a real backend.
So I created:
Cognito User Pool
App Client
Cognito Hosted Login Page
JWT Authorizer in API Gateway
When an unauthenticated request is sent to the protected POST /orders route, the API returns:
{
"message": "Unauthorized"
}
This proves that API Gateway is rejecting anonymous requests before they reach Lambda.
That is a strong security boundary.
IAM Least Privilege
During development, I temporarily used broader DynamoDB permissions to move quickly.
But after the project was working, I replaced full DynamoDB access with a custom least-privilege policy.
The Lambda execution role was allowed only these actions:
dynamodb:PutItem
dynamodb:GetItem
dynamodb:DeleteItem
dynamodb:Scan
and only on the orders table.
This is very important.
A Lambda function that only needs to access one table should not have permission to modify every DynamoDB table in the account.
This follows the principle of least privilege.
CloudWatch Monitoring
A system is not production-ready if we cannot observe it.
So I used CloudWatch Logs to inspect Lambda executions and debug issues.
I also created a CloudWatch alarm for Lambda errors.
Alarm name:
CreateOrderFunction-Errors-Alarm
The alarm checks the Lambda Errors metric.
If the function records one or more errors within a one-minute period, the alarm can trigger an action.
SNS Alerting
To make the monitoring useful, I connected the CloudWatch alarm to Amazon SNS.
The alerting flow is:
Lambda error
↓
CloudWatch metric
↓
CloudWatch alarm
↓
SNS topic
↓
Email notification
This is closer to how real DevOps teams monitor production systems.
It is not enough to only log errors. Someone needs to be notified when something breaks.
API Throttling
I also configured API Gateway throttling.
Settings used:
Burst limit: 50
Rate limit: 100
Throttling protects the API from:
accidental request loops
basic abuse
sudden spikes
uncontrolled client traffic
This is an important production control.
Even if the backend is serverless and scalable, we should still control how much traffic can hit the API.
Problems I Faced and Fixed
This project was not smooth from start to finish. I ran into multiple real-world issues.
- DynamoDB table name mismatch
My code used:
Orders
but the actual table name was:
orders
DynamoDB table names are case-sensitive.
This caused a ResourceNotFoundException.
- Primary key mismatch
The DynamoDB partition key was:
OrderId
but my first Lambda code used:
orderId
That caused a validation error because DynamoDB expected the exact key name.
- Decimal serialization issue
DynamoDB returns numbers as Decimal objects in Python.
When returning an item through API Gateway, Python failed with:
Object of type Decimal is not JSON serializable
I fixed this by adding a custom serializer to convert Decimal values before returning the JSON response.
- Cognito Unauthorized response
After attaching the Cognito authorizer, my POST /orders request started returning:
{ "message": "Unauthorized" }
At first, this looked like an error.
But it was actually the correct behavior because the route was protected and no JWT token was being sent.
That helped me understand authentication at the API Gateway layer.
Why This Project Matters
This project helped me move beyond simply deploying a Lambda function.
I learned how multiple AWS services work together:
Authentication
↓
API routing
↓
Business logic
↓
Database
↓
Monitoring
↓
Alerting
↓
Security controls
This is the difference between building a demo and designing a real cloud backend.
Final Repository Structure
serverless-order-management-system/
│
├── README.md
├── architecture-diagram.png
│
├── lambda-functions/
│ ├── create_order.py
│ ├── get_order.py
│ ├── list_orders.py
│ └── delete_order.py
│
├── screenshots/
│ ├── API Gateway Routes.png
│ ├── API Throttling.png
│ ├── Architecture diagram.png
│ ├── Cloudwatch Alarm.png
│ ├── Cognito Authorizer.png
│ ├── DynamoDB- Explore Items.png
│ ├── DynamoDB order Items overview.png
│ ├── IAM least privilage role.png
│ ├── Lambda Functions.png
│ ├── POSTMAN - Get all orders.png
│ └── POSTMAN - Unauthorized.png
│
└── docs/
└── blog-draft.md
What I Would Improve Next
If I continue improving this project, I would add:
Terraform for Infrastructure as Code
GitHub Actions CI/CD pipeline
Separate dev and prod environments
Custom domain for API Gateway
AWS WAF
Full JWT token testing in Postman
More detailed CloudWatch dashboards
Unit tests for Lambda functions
Better DynamoDB access patterns for larger workloads
Final Thoughts
This project gave me practical experience with building a secure, monitored, serverless backend on AWS.
The biggest learning was not just writing Lambda code.
The biggest learning was understanding how cloud components fit together:
API Gateway exposes the backend
Cognito protects the API
Lambda runs the business logic
DynamoDB stores the data
IAM controls permissions
CloudWatch observes the system
SNS sends alerts
Throttling protects the API
This is the kind of architecture thinking I want to keep building as I continue growing as a Cloud and DevOps Engineer.
GitHub Repository: Serverless Order Management System
Portfolio: kamleshcloud.com
LinkedIn: Kamlesh Dubale
