Urgent.News

What's breaking now, across thousands of outlets.

Tech

Building a multi-tenant POS and inventory system with Spring Boot and Angular: three design decisions that paid off

Small shops need three things from software: sell quickly, know what is in stock and trust the numbers. I built a self-hosted inventory, point-of-sale and purchasing system for them, with Spring Boot 4 (Java 25), Angular 21 and PostgreSQL, running in Docker. It works in English, Arabic (right-to-left) and French, and every shop is an isolated tenant. This post is not a feature tour. It covers…

Small shops require three key features from software: quick sales, accurate inventory tracking and reliable numbers. To meet those needs, I created a self-hosted inventory, point-of-sale and purchasing system for them using Spring Boot 4 (Java 25), Angular 21 and PostgreSQL, which can operate in multiple languages including English, Arabic and French.

Each shop is isolated as its own tenant. This article does not provide a feature tour but focuses on three design decisions I would make again, with the actual code behind each.

1. Tenants isolated from each other

The biggest bug in a multi-tenant system is when a developer forgets to add a filter for tenant_id. If that happens, one shop could see another shop's sales. To avoid this, I implemented a Hibernate filter using an aspect that is switched on before every service method runs. The tenant ID comes from the user's JWT and is stored in a TenantContext.

Entities that use the filter are automatically restricted, while global tables like subscription plans are skipped. This approach ensures the rule is enforced in one place and new modules inherit it automatically. However, it is implicit, so any code running outside the service layer (like scheduled jobs or raw SQL) needs additional care.

2. An audit trail for stock changes

The product table has a quantityOnHand column, but it's tempting to just update it directly. Instead, I created an append-only stock_movements table where every change is recorded. Positive quantities represent stock coming in, while negative quantities indicate stock going out. These entries are only added through specific flows like catalog adjustment, purchase order receipt, and sale creation, ensuring consistency with CatalogItem#getQuantityOnHand().

This creates a per-item history and provides a debugging tool to identify which sale or purchase order caused any discrepancies in the numbers.

3. A reliable checkout process even without Wi-Fi

In real-world scenarios, Wi-Fi connections may be unreliable. To ensure uninterrupted sales, I implemented an offline queue in the Angular POS system. When the browser reports online again, the queued cash and other sales are replayed. Only cash and other sales are queued, while card payments require a live connection to create and confirm a payment intent.

The receipt is a preview, and the server assigns real invoice numbers upon sync. If the server rejects a queued sale due to stock shortages, it moves to a failed sales list for the cashier to review. The UI reacts to connectivity status, queue length and failed sales, ensuring a smooth and transparent checkout process.

Written by urgent.news from Dev.to's reporting — not their text. Machine-written — may contain errors; check the original before relying on it.

Read the original at dev.to →

More in Tech

More from Saturday 3 October →