Skip to content

Latest commit

 

History

History
289 lines (220 loc) · 11.8 KB

File metadata and controls

289 lines (220 loc) · 11.8 KB
image

POS System

A robust, offline-first, cross-platform Point-of-Sale system built for real-world restaurant operations.

Kotlin Compose Architecture Pattern


Why This Project Exists

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.


Demo

Pos.System.Demo.mp4


Core Features

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

Engineering Approach

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.

Phase 1 — Understanding the Problem

The user is a cashier. Not a developer, not a manager — a cashier serving customers in real time.

Their needs in order of importance:

  1. Speed — they'll memorize item locations within days; the system must keep up.
  2. Reliability — a crash mid-order, or a frozen screen, damages trust immediately.
  3. 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.

Phase 2 — Domain Modeling

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.totalPrice is computed, not stored: Computed property (items.sumOf { it.totalPrice }) means it is always correct by definition.
  • OrderItem is a snapshot: Stores product name and price at the moment of order. Order history remains accurate even when menu prices change.
  • Order content is immutable after confirmation: Only syncStatus changes. 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

Phase 3 — Thinking About Failures First

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.

Phase 4 — Architecture

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.

Phase 5 — State Modeling

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: Boolean

After (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.


Technical Stack

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

Design System & Adaptive Layout

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

Project Structure

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

Sync Strategy

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.


Screens

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

Future Roadmap

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

Getting Started

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