Most applications start as simple CRUD systems - create, read, update, delete. But the moment you try to model something that reflects real-world operations, that simplicity disappears.
I set out to build a basic ordering app, but somewhere along the way, I realised the real problem wasn't placing orders, it was handling what happens after.
This is a breakdown of how that shift happened and how I designed it using a serverless architecture on AWS.
At the beginning, the idea was straightforward.
A customer visits the site, browses the menu, places an order, and that’s it. But that “that’s it” didn’t sit well with me.
Because in a real setting, placing an order is just the starting point. Someone has to prepare it. Someone has to hand it over. Someone has to track what’s going on.
So I moved on from the whole simple ordering app to an actual system that handles things end-to-end.
Order lifecycle and system flow
The system ended up revolving around a defined order lifecycle.
Online orders go through the full flow, starting from PENDING, while walk-in or phone orders (handled by a waiter) start directly at PAID.
Each stage represents an actual step in how the business operates, so transitions are controlled rather than free-form.
How orders move through the system
Orders can come from two places.
One is the customer-facing app. The other is the waiter interface, which acts like a POS for walk-ins and phone orders.
Regardless of where they come from, everything goes into the same system and follows the same flow.
Once an order is placed and payment is confirmed, it becomes visible to the kitchen. From there, it moves through preparation, becomes ready, and then gets handled based on whether it’s pickup, dine-in, or delivery.
Keeping everything in one flow avoided a lot of fragmentation early on.
Role-based responsibilities
Instead of allowing anyone to update anything, I split responsibilities across roles.
The chef handles preparation. The waiter handles dispatch, serving, and delivery. The admin and manager oversee operations and manage users. The customer just initiates the process.
This might sound obvious, but enforcing it properly makes a big difference. It prevents conflicting updates and keeps the workflow predictable.
System architecture on AWS
On the infrastructure side, I went with a serverless setup on AWS.
The frontend is hosted on S3 and delivered through CloudFront. All API requests go through API Gateway into Lambda functions, which handle the business logic. Data is stored in DynamoDB.
Here’s a simplified view:
Other services fit around this core.
Cognito handles authentication and roles. Secrets Manager stores API keys. Payments are handled through Paystack. Emails are sent via SES/Brevo (as a fallback).
Most of the infrastructure setup was handled using Kiro, which helped speed up provisioning and kept things consistent as the system evolved.
Data modeling with DynamoDB
DynamoDB forced a different way of thinking.
Instead of designing tables around entities, I had to think about how the data would be accessed.
The most common queries were things like:
- what’s currently preparing
- what’s ready
- what’s out for delivery
So the structure was built around that.
Orders include fields like status, createdAt, serviceType, and linkedTo. A secondary index on status makes it easy to fetch orders in a particular stage without scanning the entire table.
Handling real-world user behavior
One part that got complicated was user behavior.
A customer places an order, then comes back to place another. Now the system has to decide what to do with that.
Do you treat it as a separate order? Do you link it? What if the service type is different?
I ended up adding a linking option to let users decide whether to merge orders or keep them separate. There are also restrictions-once an order reaches a certain stage, you can’t change things like service type anymore.
That keeps things from breaking operationally.
Kitchen and dispatch workflow
The kitchen view is simple on purpose.
New orders appear in a single section. Orders being prepared show up in another. Each order has a timer, so it’s easy to see how long it has been sitting.
The waiter interface takes over after that.
It handles dispatch for pickup orders, serving dine-in customers, and managing deliveries.
It also handles orders that never came through the website in the first place.
Everything still ends up in the same system, which keeps things consistent.
API design and state enforcement
The backend is not just CRUD.
Every action goes through validation.
You can’t skip states. You can’t move an order to completed if it hasn’t been prepared. You can’t change certain properties once the order is too far along.
Each role can only perform specific actions.
That turned the backend into more of a state machine than a typical API.
Lessons learned
Most of the complexity in this project didn’t come from the infrastructure. It came from trying to match how things actually work in the real world.
Defining the order lifecycle was one thing. Handling edge cases—like multiple orders, changing service types, or different order sources—was where most of the effort went.
Enforcing rules at the backend made a big difference. It’s easy to rely on the frontend to guide users, but that falls apart quickly. Once the backend enforces the flow, things stay consistent.
Working with DynamoDB also required a shift in thinking. It’s less about structuring data neatly and more about making sure your queries are efficient.
Finally, splitting responsibilities across roles wasn’t just about access control. It shaped how the system behaves and prevented a lot of conflicts.
Closing thoughts
This project didn’t become interesting because of the stack.
It became interesting when it started behaving like something that could actually be used.
Once multiple people interact with a system, and actions happen at different times, structure becomes necessary. Without it, things fall apart quickly.
If you’re building something similar, it’s worth asking:
Are you just exposing endpoints… or are you designing how things actually work?






Top comments (0)