A robust, offline-first, cross-platform Point-of-Sale system built for real-world restaurant operations.
Most POS systems fail cashiers in the same three ways:
| The Problem | The Real Cost |
|---|---|
| Manual price lookups | Slows service, creates errors under pressure |
| No availability awareness | Cashier promises items that aren't there |
| Internet dependency | One network drop stops all operations |
This system was designed to eliminate all three — not by adding features, but by understanding the cashier's actual workflow before writing a single line of code. Designed to work reliably under real-world constraints: slow networks, sudden crashes, and high-speed service pressure.
Pos.System.Demo.mp4
- Lightning Fast Workflow: Optimized for speed so cashiers can serve customers without UI delays.
- Offline-First Resilience: The system runs completely offline and queues sync operations in the background. Network drops won't stop operations.
- Adaptive UI: Beautifully scales from mobile phones to tablets and desktop monitors using Compose Multiplatform.
- Hardware Ready: Built with hardware integration points for receipt printers and cash drawers.
- Order Management: Immutable order histories, fail-safe sync strategies, and clear state management.
Before opening an IDE, deliberate time was spent on system design. The thinking happened in phases, each one feeding the next:
Problem Space → Domain Model → Data Flows → Failure Modes → Architecture → Code
This isn't a methodology for its own sake. It's how you avoid the most expensive kind of bug: building the wrong thing correctly.
The user is a cashier. Not a developer, not a manager — a cashier serving customers in real time.
Their needs in order of importance:
- Speed — they'll memorize item locations within days; the system must keep up.
- Reliability — a crash mid-order, or a frozen screen, damages trust immediately.
- Clarity — errors must be obvious; ambiguity causes mistakes.
This shaped every subsequent decision. A feature that adds power but slows the cashier down is a net negative.
Core user journey:
Open POS → Browse by category or search → Add items to cart
→ Review totals → Confirm order → Print receipt → Sync in background
Simple on the surface. The engineering challenge lives in what happens when each step goes wrong.
The domain model answers one question: what is always true in this system?
Category (1) ──── (*) Product
│
CartItem ← mutable, quantity changes
│
(confirm: snapshot)
│
OrderItem ← immutable, historical record
│
Order ← content immutable, sync state mutable
Key modeling decisions and why:
Cart.totalPriceis computed, not stored: Computed property (items.sumOf { it.totalPrice }) means it is always correct by definition.OrderItemis a snapshot: Stores product name and price at the moment of order. Order history remains accurate even when menu prices change.Ordercontent is immutable after confirmation: OnlysyncStatuschanges. Enforces the business rule that a confirmed transaction cannot be silently altered.- Business rules encoded at the domain level:
- ✗ An order cannot exist without items
- ✗ Cart item quantity cannot be zero or negative
- ✗ An unavailable product cannot be added to cart
- ✗ A synced order cannot be modified
- ✗ Every order carries a UUID for idempotency
Reliability was a top-1 priority, so failure scenarios were designed before the happy path was built.
| Scenario | System Behavior |
|---|---|
| Internet drops mid-session | System continues fully offline; sync queues automatically |
| App crashes during active order | V1: restart cleanly; V2: offer to restore session |
| Duplicate sync attempt | Idempotency-Key header (order UUID) prevents double-processing |
| Printer fails | Receipt reprint available from order history at any time |
| Sync keeps failing | Exponential backoff → max retries → manual retry option in UI |
Offline-first is not a feature — it's a constraint. The local database is always the source of truth. The UI reads from local DB (always fast). Remote sync happens in the background.
The architecture is a direct output of the domain and failure thinking.
┌─────────────────────────────────────────────────┐
│ Presentation Layer │
│ ViewModel · MVI Contract · Compose UI │
│ (Shared across Android, iOS, Desktop via CMP) │
└──────────────────────┬──────────────────────────┘
│ calls Use Cases only
┌──────────────────────▼──────────────────────────┐
│ Domain Layer │
│ Use Cases · Domain Models · Repository │
│ Interfaces · Business Rules │
│ (Pure Kotlin — zero platform dependencies) │
└──────────────────────┬──────────────────────────┘
│ implements interfaces
┌──────────────────────▼──────────────────────────┐
│ Data Layer │
│ SQLDelight (local) · Ktor (remote) │
│ Repository Implementations · Mappers · DTOs │
└─────────────────────────────────────────────────┘
Why Clean Architecture: Zero framework imports in the domain layer means every use case, rule, and model is testable with plain Kotlin. MVI over MVVM: MVI's contract (State, Event, Effect) makes every possible UI state visible and exhaustive, preventing impossible states.
The most common source of UI bugs is impossible states.
Before (bug-prone):
// These can combine into 4 states, 2 of which make no sense
val isLoading: Boolean
val hasItems: BooleanAfter (type-safe MVI):
sealed interface CartState {
data object Empty : CartState
data class HasItems(val cart: Cart) : CartState
data class CheckingOut(val cart: Cart) : CartState
data object Confirmed : CartState
}The compiler prevents Empty + CheckingOut. Same pattern is applied for Sync Statuses.
| Layer | Technology | Rationale |
|---|---|---|
| Language | Kotlin Multiplatform | Single codebase for Android, iOS, Desktop |
| UI | Compose Multiplatform | Shared UI — one implementation for all platforms |
| Presentation | MVI + ViewModel | Predictable state, explicit transitions |
| Local DB | SQLDelight | Type-safe SQL, multiplatform, compile-time verified |
| Networking | Ktor Client | Multiplatform HTTP |
| DI | Koin | Multiplatform-compatible, minimal boilerplate |
| Async | Coroutines + StateFlow | Structured concurrency, lifecycle-aware collection |
Material3 is used as infrastructure, not as a design language.
All UI components are wrapped in a custom design system (PosTheme) that exposes only project-specific tokens.
Adaptive Layout: The UI adapts to three form factors from a single codebase:
- Compact
< 600dp: Single column · cart in bottom sheet · stacked payment flow - Medium
600–840dp: Two-column grid · slide-out cart panel - Expanded
> 840dp: Left nav · product grid · right cart/detail panel
shared/
├── domain/ ← Pure Kotlin. No framework imports.
│ ├── model/ ← Cart, Order, Product, OrderItem...
│ ├── usecase/ ← One class, one responsibility
│ └── repository/ ← Interfaces only
│
├── data/
│ ├── local/ ← SQLDelight + local data sources
│ ├── remote/ ← Ktor + DTOs + remote data sources
│ ├── mapper/ ← DTO ↔ Entity ↔ Domain conversions
│ └── repository/ ← Implementations of domain interfaces
│
└── presentation/
├── designsystem/ ← PosTheme, tokens, components
├── adaptive/ ← WindowSizeClass
└── screens/
├── menu/ ← MenuScreen · MenuViewModel · MenuContract
├── orders/ ← OrdersScreen · OrdersViewModel · OrdersContract
└── payment/ ← PaymentScreen · PaymentViewModel · PaymentContract
Order confirmed
│
▼
Saved to Local DB (syncStatus = PendingSync)
│
▼
Background coroutine picks up pending orders → HTTP POST with Idempotency-Key
│
┌─┴─┐
200 OK Network Error / 5xx
│ │
▼ ▼
Synced retryCount < 3? ──(Yes)──> Retry after exponential backoff
│
(No)
▼
SyncError (manual retry available in UI)
The Idempotency-Key header ensures that if a sync request succeeds on the server but the response is lost, retries won't create duplicate orders.
- Menu / POS: Browse products, add to cart, real-time totals, proceed to payment.
- Order History: Full transaction log, filter by status, tap to inspect detail, reprint receipts.
- Payment: Order summary, payment method selection, cash numpad with quick amounts, process payment.
- Multi-branch support: Central server resolves conflicts, branch-level analytics.
- Session recovery: Offer to restore incomplete cart after a crash.
- Offline product sync: Versioned product catalog to only transfer deltas.
- Logging and observability: Instrument Use Case boundaries.
# Clone the repository
git clone https://github.com/Ahmedsayed0895/POSSystem.git
cd POSSystem
# Run Android App
./gradlew :androidApp:installDebug
# Run Desktop App
./gradlew :desktopApp:run
# Run iOS App
# Open iosApp/iosApp.xcodeproj in Xcode and hit Run
by Ahmed Sayed.
Passionate about crafting reliable, scalable, and beautifully engineered software solutions.