Building Zomoto: A Full-Stack Restaurant POS System with Flutter
How I built my first Flutter application — a multi-role restaurant POS — and what I learned along the way.
Zomoto is a restaurant point-of-sale system I built entirely in Flutter. It started as my first Flutter project — a way to learn the framework by solving a real problem. What began as a learning exercise has grown into a production application now running in 3 restaurants in Sri Lanka.
This post is a deep dive into what the app does, how it works under the hood, the big decisions I made, and where I'm taking it next.
The Problem I Wanted to Solve
If you've ever worked in or run a restaurant, you know the chaos. Orders get lost between the front of house and kitchen. Waiters scribble on paper. The cashier is doing mental math. The owner has no idea what's selling.
Most existing POS solutions are either:
- Expensive — subscription-heavy cloud systems that bleed small restaurants dry
- Rigid — one-size-fits-all tools that don't adapt to how your restaurant actually works
- Disconnected — separate apps for kitchen, billing, and management that don't talk to each other
I wanted to build one app where every person in the restaurant — the owner, the cashier, the cook, the waiter — logs in and sees exactly what they need. Nothing more, nothing less. And I wanted to learn Flutter by building it.
What Zomoto Does Today
One App, Five Roles
The core idea is simple: everyone uses the same app, but the experience changes based on who you are.
The Cashier gets a full POS terminal. They browse the menu by category, add items to a cart, choose whether it's a dine-in, takeaway, or delivery order, and fire it off to the kitchen. For dine-in, they pick a table from a live grid — the app knows which tables are free and which are occupied. If a table already has an active order, they can add another round instead of creating a new one.
The cashier also handles billing. They see every order's status in real time — which rounds the kitchen has started, which are ready, which items are still being prepped. They can only complete an order once the kitchen marks everything as ready.
The Kitchen sees a dedicated display — think of it as a digital ticket rail. Orders come in, grouped into two panels: one for takeaway/delivery, one for dine-in organized by table. Each kitchen station only sees its own items. If you're on the grill, you see grill items. If you're on desserts, you see desserts. You tap "Start Cooking" when you begin, "Mark Ready" when it's done.
The Waiter sees a table grid. Tap a table, see its current order, or place a new one. They get the same menu browsing experience as the cashier — categories, variants, modifiers, special notes — but scoped to their workflow.
The Owner and Manager share an admin dashboard with tabs for Orders, Menu, Office, and Reports. The Office tab is where the real management happens — staff management, table configuration, kitchen station setup, device registration, modifiers, variations, customer records.
Real-Time Everything
This isn't a polling-based system. When the kitchen marks an item as ready, the cashier's screen updates instantly. When a waiter places a dine-in order, the table grid flips from green to red in real time. Every order, every status change, every table update flows through Firestore streams.
Multi-Round Dine-In Orders
In a real restaurant, a table doesn't place one order — they place several. Starters first. Then mains. Maybe a dessert later. Zomoto models this with rounds. Each order can have multiple rounds, each round has its own items, and each item has its own kitchen status. It's a three-level status hierarchy that actually mirrors how food service works.
Multi-Kitchen Routing
Not every restaurant has one kitchen. Many have separate stations — a main kitchen for hot food, a bar for drinks, a dessert station. Every menu item in Zomoto can be assigned to a specific kitchen. When an order comes in with items from three different stations, each kitchen only sees their items.
The Tech Behind It
Flutter + Clean Architecture
I chose Flutter because I wanted to learn it by building something real — not just following tutorials. A POS system seemed like the perfect challenge: complex enough to push me, practical enough to be useful.
The codebase follows Clean Architecture with four packages:
- domain — Pure Dart. Zero Flutter imports, zero Firebase imports. Just entities, enums, repository interfaces, and failure types.
- data — Repository implementations, DTOs, and mappers. Every repository method returns Either<Failure, T> — no exceptions leaking to the UI.
- firebase_data — The Firebase-specific implementations. If I ever swap Firebase for something else, only this package changes.
- local_data — SharedPreferences for session management, cart persistence, and caching.
As I continue enhancing Zomoto, Clean Architecture has been invaluable. Adding new features doesn't feel like fighting the codebase.
State Management: Riverpod
I use Riverpod throughout. StreamProvider for anything real-time (active orders, table statuses). StateNotifier for complex mutable state (the cart, auth state). FutureProvider for one-shot reads.
Firebase: Firestore + Realtime Database + Auth
Active orders live in a Firestore collection. When an order is completed or cancelled, it gets archived — copied to a separate collection and deleted from the active one. This keeps the active collection small and fast.
Restaurant configuration lives in Firebase Realtime Database because it's a flat document that needs to be cheap to read frequently.
Material 3 Theming
The app uses Material 3 with a custom theme system. Semantic colors for order statuses — amber for pending, blue for preparing, green for ready, red for cancelled. Both light and dark themes are supported.
What I Changed Recently
Rewrote the Order Placement Pipeline
The biggest refactor was moving order creation logic out of the UI layer into the Firebase data layer. Previously, the screens were responsible for generating order IDs, resolving the current user, setting timestamps. Now, the UI just sends a list of items and the order type. The data layer handles everything else.
Built the Split Bill Feature
A full-screen dialog with two panels. The left side shows all order items with quantities. The right side shows person cards. Tap a person to select them, then tap items to assign. Each person's card shows their assigned items and a running subtotal. The "Complete Order" button only enables when every item has been assigned.
Rebuilt the Kitchen Display
The dual-panel layout now properly separates ongoing orders from table orders. Kitchen station filtering works — each kitchen user only sees their items. Ready rounds auto-dismiss after five seconds to keep the screen clean.
What's Coming Next
Near-Term
- Payment integration — card payments, mobile payments, cash drawer management
- Receipt printing — the printer infrastructure is there (Bluetooth and USB), just needs wiring up
- Real-time notifications — push alerts when food is ready or new orders arrive
- Offline support — proper queue handling when the internet drops mid-service
Medium-Term
- Discount and promo system — percentage discounts, promo codes, happy hour pricing
- Customer profiles and loyalty — order history, loyalty points, tier-based rewards
- Reporting and analytics — daily revenue charts, best-selling items, peak hour analysis
Long-Term
- Multi-branch support — manage multiple restaurant locations from one admin panel
- Localization — Sinhala, Tamil, and English language support
- Table reservation system
- Stock and inventory management
Lessons Learned
Clean Architecture pays off in POS systems. The number of times I've been able to change how data flows without touching the UI has justified every minute spent setting up the layer boundaries.
Model your domain after real-world operations. The three-level status hierarchy (order > round > item) only makes sense when you've watched how a real kitchen works. Let the real world drive your data model.
Real-time is non-negotiable for restaurant software. A POS where you need to refresh to see updates is a POS that gets abandoned.
Role-based UX is different from role-based access control. It's not just about who can see what — it's about building completely different experiences for different users within the same codebase.
Learn by building something real. Zomoto was my first Flutter project. Building it for actual restaurants forced me to deal with edge cases, performance constraints, and user expectations that tutorials never cover. It's the most effective way to learn a framework.
Tech Stack Summary
| Layer | Technology |
|---|---|
| Framework | Flutter 3.x + Dart 3.x |
| Architecture | Clean Architecture (4-layer monorepo) |
| State Management | Riverpod |
| Navigation | GoRouter with role-based shell guards |
| Database | Firebase Firestore + Realtime Database |
| Authentication | Firebase Auth |
| Local Storage | SharedPreferences |
| UI Framework | Material 3 with custom theme system |
| Printing | ESC/POS via Bluetooth & USB thermal printers |
| Error Handling | Either<Failure, T> (functional) |
Wrapping Up
Zomoto started as a way to learn Flutter and turned into something real — a production POS system running in 3 restaurants in Sri Lanka. The core flows — ordering, kitchen management, billing, table management — are solid. I'm actively enhancing features and refining the architecture as the system grows.
If you're building something similar, or if you're a restaurant owner curious about what modern POS software looks like from the inside, I'd love to hear from you.
Built with Flutter. Powered by Firebase. Designed for real restaurants.