# 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](https://github.com/KamleshDevopsAndCloud/project-3-serverless-order-management-.git)  
Portfolio: [kamleshcloud.com](http://KamleshCloud.com)  
LinkedIn: [Kamlesh Dubale](https://www.linkedin.com/in/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.

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/64276c1c-84cd-4cc4-9a85-645fc7f11ad4.png align="center")

## High-Level Architecture

The request flow looks like this:

```plaintext
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 |

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/74613131-4304-4782-9ba8-16dd7ea34951.png align="center")

This route structure follows a simple REST-style API design.

For example:

```plaintext
POST /orders
```

means create an order.

```plaintext
GET /orders
```

means list all orders.

```plaintext
GET /orders/{id}
```

means retrieve one specific order.

```plaintext
DELETE /orders/{id}
```

means delete one specific order.

## Lambda Functions

Instead of using one large Lambda function for everything, I created separate Lambda functions:

<table style="min-width: 50px;"><colgroup><col style="min-width: 25px;"><col style="min-width: 25px;"></colgroup><tbody><tr><td colspan="1" rowspan="1"><p>Lambda Function</p></td><td colspan="1" rowspan="1"><p>Responsibility</p></td></tr><tr><td colspan="1" rowspan="1"><p>CreateOrderFunction</p></td><td colspan="1" rowspan="1"><p>Creates a new order</p></td></tr><tr><td colspan="1" rowspan="1"><p>GetOrderFunction</p></td><td colspan="1" rowspan="1"><p>Gets one order</p></td></tr><tr><td colspan="1" rowspan="1"><p>ListOrdersFunction</p></td><td colspan="1" rowspan="1"><p>Lists all orders</p></td></tr><tr><td colspan="1" rowspan="1"><p>DeleteOrderFunction</p></td><td colspan="1" rowspan="1"><p>Deletes an order</p></td></tr></tbody></table>

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/b0fb2252-f6a8-4c6a-a2e2-953bb0dc6b96.png align="center")

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:

```plaintext
orders
```

The partition key is:

```plaintext
OrderId
```

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/da6b9dfa-bedf-4700-a979-22edd9db18b2.png align="center")

Each order item contains:

```plaintext
{
  "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`.

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/a465e06a-fc0a-4992-b91c-f661e3d5c8ae.png align="center")

## Create Order Flow

When a client sends this request:

```plaintext
POST /orders
```

with this JSON body:

```plaintext
{
  "customerName": "Kamlesh",
  "product": "MacBook Pro",
  "quantity": 1
}
```

the flow is:

```plaintext
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:

```plaintext
customerName
product
quantity
```

If the request body is missing required fields, the API returns a proper error response instead of crashing.

Example bad request:

```plaintext
{
  "customerName": "Kamlesh"
}
```

Response:

```plaintext
{
  "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.

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/dd577501-b86a-4cbc-a40c-f7c877459e6e.png align="center")

The response looked like this:

```plaintext
[
  {
    "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:

```plaintext
GET /orders/994dd3bc-a43a-4342-b07b-3f5829e3c046
```

API Gateway passes the ID to Lambda inside:

```plaintext
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:

```plaintext
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
    

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/a7048275-4505-4a69-9b69-e8fcf13be95a.png align="center")

When an unauthenticated request is sent to the protected `POST /orders` route, the API returns:

```plaintext
{
  "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:

```plaintext
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:

```plaintext
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:

```plaintext
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:

```plaintext
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.

1.  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.

2.  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.

3.  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.

4.  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:

```plaintext
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

```plaintext
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
    

* * *

![](https://cdn.hashnode.com/uploads/covers/69d24c9340c9cabf44d0c3dd/dc89a149-a4e7-4a88-ac4a-ec431ca2fd2b.png align="center")

## 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](https://github.com/KamleshDevopsAndCloud/project-3-serverless-order-management-.git)  
Portfolio: [kamleshcloud.com](http://KamleshCloud.com)  
LinkedIn: [Kamlesh Dubale](https://www.linkedin.com/in/kamlesh-dubale)
