{
  "id": 11661412,
  "title": "Building a multi-tenant POS and inventory system with Spring Boot and Angular: three design decisions that paid off",
  "url": "https://urgent.news/2026/10/03/building-a-multi-tenant-pos-and-inventory-system-with-spring-boot-and",
  "topic": "tech",
  "section": "Tech",
  "published": "2026-10-03T10:33:24.000Z",
  "source": {
    "name": "Dev.to",
    "slug": "dev-to",
    "url": "https://dev.to/mfarouk2894/building-a-multi-tenant-pos-and-inventory-system-with-spring-boot-and-angular-three-design-end"
  },
  "original_language": "en",
  "account": "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.\n\n1. Tenants isolated from each other\nThe 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.\n\n2. An audit trail for stock changes\nThe 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.\n\n3. A reliable checkout process even without Wi-Fi\nIn 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.",
  "summary": "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…",
  "key_points": [
    "Tenants isolated from each other using Hibernate filter and tenant ID from JWT",
    "Audit trail for stock changes with append-only stockmovements table",
    "Reliable checkout process with offline queue for unreliable Wi-Fi"
  ],
  "editors_take": null,
  "illustration": null,
  "coverage": {
    "outlets": 1,
    "also_reported_by": []
  },
  "ai_generated": true,
  "disclaimer": "Summaries, key points and the editor’s take are written by software from other outlets’ reporting and may contain errors — always check the linked original."
}