Skip to content
Case study 02, all workRetail operations platform

Polaris

2024 to nowIn daily use by retail shops

Retail operations software for shops that bill all day: invoices, refunds, batch stock, supplier orders and customer ledgers that have to agree with each other.

RoleBuilt it end to end

The invoice screen. Screens here are halftoned on purpose, so no customer data survives.

Team
Just me, through Commit Software
Timeline
2024 to now
Stack
Django, Django REST Framework, PostgreSQL, Redis, Celery, Vue 3, Pinia, TanStack Query
Source
Private, client work

What came of it

  1. 01Billing, refunds, stock and ledgers stay consistent with several cashiers working at once
  2. 02Every organization's data isolated at the database level
  3. 03Supplier price lists imported from CSV, Excel and PDF files as they arrive

Polaris started as client work through Commit Software and turned into the system several retail shops run their day on. It covers the parts a shop touches constantly: invoices and quotations, refunds, stock, customer credit, supplier purchasing, expenses, reports, and the admin around all of it.

Most of the work is in the edge cases. A billing screen has to stay right when two cashiers sell the last item, a customer returns half an order on credit, and a supplier's price list arrives as a scanned PDF.

The home screen. Every tile is a workflow that has to agree with the others.

Stock derived from batches

A product's quantity is always derived from batch records, never typed in: what came in, from whom, at what cost, and what's left. That rule made every other module stricter. Billing, restocking, cycle counts and supplier returns all have to go through the same contract, and none of them can "just fix the number".

It costs more work up front, and it's why stock counts in Polaris can be trusted at the end of the day.

Several cashiers at once

The race conditions came first, as they do. Two cashiers selling the last unit took stock negative, and two operations on one customer could leave a balance that matched neither. The war stories post has the details. These are the fixes that stuck:

  • row locks that fail fast and retry briefly instead of queueing, so a cashier sees a blip at worst
  • version checks, so a stale form can't overwrite newer edits
  • per-customer and per-supplier advisory locks for ledger work, with keys that mean the same thing on every worker process
  • tests that pin those contracts down, including one that checks lock keys come out the same under different hash seeds

Tenants separated in the database

Polaris is multi-tenant. The organization is set once per request and carried down into the database session, so the separation doesn't depend on every query remembering a filter.

Supplier files arrive however suppliers like

Price lists come as CSV, Excel and PDF, with inconsistent names, missing units and layouts that change without warning. The import path extracts, normalizes and matches them against the catalogue, with aliases for the names suppliers insist on using. It locks around anything that can change purchasing, because a half-applied price list is worse than none.

Generating a supplier order from low-stock items.

If I started it again

  • I'd write explicit services from the start. Early versions spread business rules across Django signals; they're now plain service functions you can read top to bottom. The performance log covers that and the other early mistakes.
  • Concurrency tests on day one. Every serious bug in Polaris was a concurrency bug that a two-thread test would have caught.
  • A written contract for every module boundary, since the worst bugs lived in the gaps between modules.
Next projectOctoberA campaign platform where brands, agencies and creators run influencer campaigns from brief to report, with numbers every side can trust.