Let's Talk

Case study

Dukaan POS

An offline-first point of sale for small shops, from one Compose Multiplatform codebase

How I designed and built a point-of-sale and billing app for small shops in Pakistan that works with no internet, in English and Urdu, on Android, iOS, desktop, and the web from one shared Kotlin codebase.

Role
Solo: product, design, engineering, and website
Platforms
Android, iOS, Windows, macOS, Linux, and web
Status
Feature-complete; field test with a real shop is next
Dukaan POS website hero showing the app's dashboard on a laptop

1

shared codebase for every platform

32,000+

lines of Kotlin

441

unit tests, all passing

2

languages, English and Urdu, both complete

Tech stack

KotlinCompose MultiplatformMaterial 3Navigation 3KoinRoom + bundled SQLiteKotlinCryptoML Kit & CameraXjSerialComm

The problem

Most small shops in Pakistan, such as kiryana stores, pharmacies, and mobile shops, keep their accounts in a paper khata or in one-time-purchase Windows billing software that never gets updated. Credit given to regular customers (udhaar) is the hardest part to track, and it is where shops quietly lose money.

The people using the app are shopkeepers and cashiers who are often not tech-savvy, work fast, and sometimes serve customers one-handed with a barcode scanner. Many read Urdu more comfortably than English, and the internet in a market is not something a sale can depend on.

That set three hard requirements: work fully offline, treat Urdu as a first-class language, and make the sell screen fast. The target was a five-item sale in under twenty seconds.

What I built

Everything a shopkeeper does in a day, on a phone, a tablet, or the computer on the counter:

  • Billing by tap, barcode scan, or search, with weighed goods, discounts, up to ten held bills, and cash, card, or credit payments.
  • Udhaar (credit) with partial payments, a warning before credit is given, a chase list ranked by promised date, and customer statements sent over WhatsApp.
  • Stock that updates with every sale, with batches and expiry dates, low-stock alerts, and product import from a spreadsheet.
  • Receipts on 58 mm and 80 mm thermal printers over Bluetooth, USB, or serial, and on A4 office printers.
  • Cashiers with their own PINs and roles, daily cash reconciliation, expenses, suppliers, and reports.
  • A single backup file that the shopkeeper keeps and can restore on another device.
Dukaan POS Reports screen on a desktop window, answering whether the shop made money this month
Reports answer five plain questions a shopkeeper actually asks, such as "Did I make money?", instead of showing a wall of charts.

Key technical decisions

  • One codebase, with platform differences kept at the edges. All shared code lives in one module. The only places a platform can differ are four dependency-injection modules: printing, files, scanning, and the database. A fix or a new feature therefore reaches every platform at once.
  • Offline-first by design. Room with a bundled SQLite driver is the single source of truth. Reads return a Flow and writes are suspend functions. Deletes are soft, so a shop's history is never lost.
  • Money is never a floating-point number. Amounts are stored as whole numbers of paisa, so totals, discounts, and change are always exact. Quantities stay decimal because weighed goods sell in fractions.
  • Urdu is part of the build, not an afterthought. Every string lives in two XML files, and a Gradle task generates the string table from them. If a key is missing in Urdu, the build fails. The shopkeeper picks the language inside the app, because iOS does not let an app override the device language at runtime.
  • Urdu receipts are printed as a picture. Thermal printers have no Urdu font, so the receipt is drawn and sent to the printer as dots. The same layout serves both 58 mm and 80 mm paper.
  • Layouts adapt to window size, never to the platform. The same screen becomes a list and detail side by side on a wide desktop window, and a single column on a phone.
  • Business rules live outside the screens. MVVM with one-way data flow keeps ViewModels about what a screen shows, while rules such as what a refund is worth live in their own package, where they are tested without any UI.
Dukaan POS dashboard on a phone in English
The phone dashboard in English.
Dukaan POS dashboard on a phone in Urdu, with a right-to-left layout and Nastaliq type
The same screen in Urdu, mirrored right to left, with Nastaliq type.

Challenges and how I solved them

  • Layout bugs that code review could not catch. A few layouts looked right in code but broke on real window sizes. I added a screenshot test that draws real screens headlessly at the widths shopkeepers use, in English and in Urdu, and fails if any layout cannot fit. It caught a clipped button row before any shopkeeper saw it.
  • Keeping dependency injection safe. Koin resolves dependencies at runtime, so a missing registration would only crash when a screen opened. One test walks the whole dependency graph and another checks every ViewModel constructor, so a missing registration fails a test instead.
  • Protecting a shop's data. A shop's database is its business. Development builds install beside the real app with their own data, destructive database resets only exist in debug builds, and a test fails as soon as the database schema changes, so it can never surprise a user as a crash on launch.

Quality

The project has 441 unit tests on the JVM, all passing, and the shared suite also runs on Android, iOS, and the web. The Room repositories are tested against real SQLite, and ViewModels are tested with injected dispatchers and fake clocks, so the tests stay deterministic.

Where it stands

The app is feature-complete for a shop that runs offline. Android is the full product, with Bluetooth printing, WhatsApp sharing, backups, and camera scanning. Desktop (Windows, macOS, and Linux) is the counter machine, with USB and serial printing and support for a barcode scanner gun. iOS runs and keeps data but does not yet print, share, or back up. The web version is a live demo that keeps nothing once the tab is closed.

It has not yet been used by a real shop, so the next step is a field test: a shopkeeper ringing up real sales and a real thermal printer printing real receipts. After that, cloud sync and multi-branch support, which need a server.

Building something similar?

I help teams ship reliable apps on Android, iOS, desktop, and web.