DEV Community

Cover image for I built a multi-branch point of sale with ASP.NET Core 10 and Angular. Here's the tour.
Zohaib Ahmad
Zohaib Ahmad

Posted on AI-assisted

I built a multi-branch point of sale with ASP.NET Core 10 and Angular. Here's the tour.

For the last few months I've been building Tilly POS, a point of sale and back office for shops and small retail chains. It's live, there's a demo you can break, and this post is the tour: what it does, how it's put together, and what I'd tell anyone building something similar.

Try it first: open the demo as the cashier or as the owner. No sign-up; it resets every night.

What it does

  • Checkout — scan or search, held sales, discounts with manager approval, split payments, receipts
  • Cash drawer — shifts with an opening float, paid in/out, a denomination count at close, manager review
  • Returns and voids — partial refunds to the original payment method
  • Stock in every branch — a ledger per item, weighted-average cost, counts, adjustments, transfers
  • Purchasing — suppliers, purchase orders, goods received
  • Reports — sales, profit, stock, purchases and cash, with CSV export
  • Administration — users, custom roles, branches and registers, and an audit log nobody can edit

The stack

Layer Choice Why
API ASP.NET Core 10 minimal APIs Small handlers, typed results, accurate OpenAPI for free
Data EF Core + SQL Server Transactions and row locks are the whole game in a POS
Front end Angular 22 (standalone, signals) + Tailwind v4 Fast, keyboard-first checkout screen
Hosting Docker Compose (SQL Server + API + nginx) One command on a small VPS
Tests xUnit + WebApplicationFactory against a real SQL Server 104 integration tests

The back end is a modular monolith: Identity, Tenancy, Catalog, Inventory, Sales, Registers, Purchasing, Reports, Billing and Audit are separate folders with their own endpoints and their own SQL schema, but one process and one database. A sale writes to sales, payments, stock and audit — and in one database that's one transaction instead of a distributed saga.

Three decisions I'd make again

1. Idempotent checkout. The browser sends a key with every sale attempt; a unique index on (business, key) makes a retried Charge return the original sale instead of charging twice.

2. The stock check is the write. "Is there enough?" is a conditional, row-locked UPDATE — if no row comes back, the sale is refused. A read-then-write check lets two tills sell the last unit.

3. Read-only, not locked out. When a trial ends or a payment fails, the business can still see and export everything; it just can't change anything. Nobody loses access to their own records over a billing problem.

I wrote up the first two in detail, with code: Three bugs a point-of-sale can't afford.

What I'd tell you if you're building one

  • Test against the real database. Every bug that matters in a POS lives in transactions, locks and unique indexes. An in-memory provider passes and proves nothing.
  • Design for the bad Wi-Fi day, not the demo. Retries, double clicks and half-finished requests are normal.
  • Make the cashier's error messages say what happened and what to do. "Only 0 of 'Basmati rice 5 kg' left in this branch. Change the quantity or remove the item; nothing was charged." beats "Error 409".

Using it

Questions about any of it — the architecture, the checkout, the stock ledger — are very welcome in the comments.

Top comments (0)