Skip to main content
Rohit Raj
होमप्रोजेक्ट्सServicesरिपॉज़नोट्सपरिचयसंपर्कवर्तमान कार्य देखें
← Back to Notes

Building a Multi-Tenant Retail SaaS with Spring Boot 3.4 + Java 21 — 12-Module Architecture

Rohit Raj·April 14, 2026·11 min read

How I architected RetailOS — an India-first multi-tenant retail platform with billing, inventory, GST invoicing, khata ledger, and offline sync in a 12-module Maven monorepo.

multi-tenant saas spring bootjava 21 spring boot 3.4retail saas indiamaven multi-module architecture

India's Retail Problem: 90% Unorganized

I built RetailOS, a multi-tenant retail SaaS platform using Spring Boot 3.4 and Java 21 with 12 Maven modules covering billing, inventory, GST-compliant invoicing, khata credit ledger, and offline sync with conflict resolution — designed specifically for India's unorganized retail sector where 90% of shops still run on paper registers and enterprise POS systems cost more than small shop owners can afford.

India's retail sector is massive — $950 billion — and 90% of it is unorganized. Kirana stores, small electronics shops, clothing boutiques, and general stores. They run on paper registers, basic billing software that can't generate GST-compliant invoices, and zero inventory tracking.

Enterprise POS systems like Zoho Inventory or Unicommerce cost ₹5,000-15,000/month — too expensive for a shop that makes ₹50,000/month profit. And they're designed for large retailers — the UI is overwhelming, the features are overkill, and there's no Hindi support.

RetailOS is built for these shops. Affordable, India-first, works offline, handles GST, and includes a khata (credit ledger) module — because 70% of Indian retail transactions involve extending credit to regular customers.

Why Choose a 12-Module Maven Monorepo?

RetailOS has 12 modules, each owning a specific domain:

text
retailos/
├── retailos-common/       # Shared kernel: tenant context, security, DTOs
├── retailos-auth/         # JWT + OTP authentication, RBAC
├── retailos-tenant/       # Tenant, Store, Warehouse, BillingCounter
├── retailos-kyc/          # Document verification, DPDP consent
├── retailos-inventory/    # Products, Categories, Stock, Movements
├── retailos-billing/      # Bills, BillItems, Payments (POS)
├── retailos-invoice/      # GST invoice generation, PDF export
├── retailos-khata/        # Credit ledger — entries, balances
├── retailos-file/         # MinIO integration for uploads
├── retailos-sync/         # Offline sync queue, conflict resolution
├── retailos-analytics/    # Dashboard metrics, reports
├── retailos-admin/        # Platform admin, impersonation
├── retailos-audit/        # Immutable event log
└── retailos-app/          # Spring Boot application entry point

Why monorepo over microservices?

For an early-stage product, microservices add deployment complexity without proportional benefit. A monorepo with clean module boundaries gives you:

  1. Single deployment — one JAR, one container, one health check
  2. Compile-time safety — if module A depends on module B's API, the compiler catches breaking changes
  3. Shared transaction context — billing + inventory + khata can participate in the same DB transaction
  4. Easy extraction later — when a module needs independent scaling, it already has clean interfaces

The modules ARE the microservice boundaries. When the time comes, extraction is straightforward because dependencies are explicit in Maven POMs.

How Does Row-Level Multi-Tenancy Isolation Work?

Multi-tenancy is the core architectural challenge. Every query, every insert, every audit log must be scoped to the current tenant.

Three approaches exist:

  1. Database-per-tenant — Maximum isolation, maximum cost. 1,000 tenants = 1,000 databases. Unrealistic for SME pricing.
  2. Schema-per-tenant — Good isolation, moderate cost. Connection pool management gets complex at scale.
  3. Row-level isolation — All tenants in one database, every table has a tenant_id column. Lowest cost, requires careful engineering.

RetailOS uses row-level isolation with a shared database. Here's how it's enforced:

TenantContext in retailos-common — a ThreadLocal that holds the current tenant ID, set by a Spring Security filter on every request:

The JWT token contains the tenant ID. The security filter extracts it, sets it in TenantContext, and every repository query automatically includes the tenant filter.

Why this works at scale: - PostgreSQL handles thousands of tenants in one database efficiently with proper indexing - Composite indexes on (tenant_id, <domain_column>) keep queries fast - Connection pooling is simple — one pool for all tenants - Flyway migrations run once, not per-tenant

The Khata Module: Credit Ledger for Indian Retail

This is the feature that makes RetailOS India-first. In Indian retail, "khata" (credit ledger) isn't a nice-to-have — it's how business actually works.

A regular customer walks in, buys ₹500 of groceries, and says "khata mein likh do" (write it in the ledger). The shopkeeper extends credit, tracked in a paper diary. Month-end, the customer settles up. Or doesn't, and the shopkeeper loses track.

retailos-khata module:

  • KhataAccount — One per customer. Tracks total credit extended, total paid, current balance.
  • KhataEntry — Every credit/debit transaction with date, amount, notes, and linked bill (if any).
  • Balance calculation — Running balance updated on every entry, verified against sum of entries.
  • Settlement flow — Partial or full payment recording with automatic balance update.

The khata module integrates directly with billing — when a bill is created with payment type "CREDIT", a KhataEntry is auto-created. No double entry needed.

Why this matters commercially: No enterprise POS has this. Zoho, Vend, Square — none of them understand credit-based retail. For an Indian kirana store owner, this single feature justifies the entire platform.

GST Invoice Generation & Offline Sync

GST Invoicing (retailos-invoice):

India's GST requires specific invoice formats with GSTIN, HSN codes, CGST/SGST/IGST splits, and sequential invoice numbering. The invoice module:

  1. Takes a completed Bill from retailos-billing
  2. Applies GST rules based on product HSN code and buyer/seller state
  3. Generates a PDF with all legally required fields
  4. Stores in MinIO for retrieval and WhatsApp sharing

Offline Sync (retailos-sync):

Internet in Tier 2/3 India is unreliable. A billing counter can't stop working because WiFi dropped. The sync module:

  1. Queue-based — Operations (bill creation, stock movement, khata entry) are queued locally
  2. Conflict resolution — When connectivity returns, queued operations sync with server. Conflicts resolved by timestamp + operation priority (payments > stock > metadata)
  3. Idempotency — Every operation has a client-generated UUID. Duplicate syncs are detected and ignored.

This is the hardest engineering problem in RetailOS. Offline sync with conflict resolution is notoriously difficult. The key insight: for retail operations, last-write-wins is acceptable for metadata (product names, prices) but additive-merge is required for transactions (you never want to lose a sale or payment).

Infrastructure & What This Demonstrates

Local development stack:

yaml
services:
  postgres:  # PostgreSQL 16, port 5432
  redis:     # Redis 7, port 6379
  minio:     # S3-compatible storage, port 9000

Production-ready features: - Swagger UI at /api/swagger-ui.html for API documentation - Actuator health checks at /api/actuator/health - JaCoCo code coverage reports - DPDP consent management in KYC module for data privacy compliance

Architecture DecisionChoiceWhy
-----------------------------------
Multi-tenancyRow-level isolationCost-effective at scale, single DB
Module structureMaven monorepo (12 modules)Clean boundaries, single deployment
File storageMinIO (S3-compatible)Self-hosted, zero cloud cost, S3-compatible for migration
AuthJWT + OTPNo password management, mobile-first
OfflineQueue + conflict resolutionTier 2/3 India internet is unreliable
InvoicingGST-compliant PDF generationLegal requirement, no shortcuts
CreditFirst-class khata moduleHow 70% of Indian retail actually works

What this project proves: You can build an enterprise-grade SaaS platform with a single engineer — if the architecture is right. 12 modules, multi-tenancy, offline sync, GST compliance — all shipping from one codebase.

Security and compliance considerations: RetailOS handles sensitive financial data — transaction records, customer credit information, and GST-linked invoices. The audit module (retailos-audit) creates an immutable event log for every data modification, which is essential for GST compliance audits. The KYC module manages DPDP (Digital Personal Data Protection) consent, ensuring that customer data collection complies with India's 2023 data protection law. JWT tokens use short expiration times with refresh token rotation, and OTP-based authentication eliminates password management entirely — critical for shopkeepers who would otherwise use "123456" as their password.

Scaling path from monorepo to microservices: The 12-module Maven structure is designed for eventual extraction. Each module communicates through service interfaces defined in retailos-common, not through direct database queries across module boundaries. When the billing module needs independent scaling — handling 10x the traffic of analytics — extraction means pulling the module into its own Spring Boot application, replacing in-process calls with REST or gRPC, and deploying independently. The module boundaries are already clean; extraction is a deployment change, not an architecture change.

Frequently Asked Questions

Q: How does RetailOS handle GST rate changes when the government updates tax slabs?

GST rates are stored as configurable data in the database, not hardcoded. When the government updates a rate for specific HSN codes, an admin updates the rate table through the admin panel. All new invoices automatically use the updated rates. Historical invoices retain their original rates for compliance — the invoice module stores the GST rate at the time of generation, not a reference to the current rate.

Q: Can RetailOS work entirely offline for days at a time?

Yes. The offline sync module queues all operations locally — bill creation, stock movements, khata entries, and payments. These operations execute locally and the UI reflects changes instantly. When connectivity returns, the queue syncs in chronological order with conflict resolution. The billing counter continues working indefinitely without internet. The only feature that requires connectivity is the initial tenant setup and authentication token refresh.

Q: How does the khata module prevent disputes between shopkeepers and customers?

Every KhataEntry has a timestamp, amount, optional notes, and a linked bill reference. When a customer disputes a balance, the shopkeeper can show the complete transaction history with bill references. The immutable audit trail in retailos-audit provides an additional layer of verification — every credit extension and payment recording is logged with the exact time, amount, and operator who processed it.

Q: What is the maximum number of tenants RetailOS can support on a single database?

With proper indexing (composite indexes on tenant_id plus domain columns), PostgreSQL efficiently handles thousands of tenants in a shared database. The practical limit depends on total data volume rather than tenant count. At 1,000 tenants with average retail volume, a single PostgreSQL instance on a moderate VPS handles the load comfortably. Connection pooling via HikariCP means all tenants share one pool.

Q: Why use MinIO instead of AWS S3 for file storage?

MinIO is S3-compatible, self-hosted, and adds zero cloud cost. For an early-stage SaaS targeting cost-sensitive Indian SMEs, every rupee of infrastructure cost affects pricing. MinIO runs alongside the application in Docker Compose. When scale demands it, migration to AWS S3 requires zero code changes because the S3 API is identical — only the endpoint URL changes.

RELATED PROJECT

View Retailos →

Need a multi-tenant SaaS platform architected from scratch? I build these.

Let's Talk →

Read Next

How I Built an Enterprise Deal Matching Platform with Spring Boot + Next.js + GPT-4o

Architecture deep-dive into SynFlow — a full-stack intelligence platform that matches deals to profi...

Cloud-First AI Is Dead. I Built a Fully Offline AI App to Prove It.

Google just shipped an offline AI dictation app. Android 16 runs notification summaries on-device. T...

← All NotesProjects →

रोहित राज — बैकएंड और AI सिस्टम्स

WhatsAppGitHubLinkedInEmail

Services

Mobile App DevelopmentAI Chatbot DevelopmentFull-Stack Development

अपडेट्स पाएं