Skip to main content
Rohit Raj
HomeProjectsServicesReposNotesAboutContactView Current Work
← Back to Notes

I Built a 12-Module Multi-Tenant SaaS Platform Alone. Here's the Architecture That Made It Possible.

Rohit Raj·April 14, 2026·11 min read

The SaaS market hits $465B in 2026 and 70% of vendors use multi-tenancy. I built RetailOS — a 12-module Spring Boot monorepo with billing, inventory, GST invoicing, khata ledger, and offline sync — as a solo engineer. Here's every architecture decision.

build multi-tenant SaaS solo developerspring boot multi-module monoreposolo SaaS architecture 2026founding engineer SaaS platform

One Engineer. Twelve Modules. Zero Shortcuts.

I built RetailOS — a complete multi-tenant retail SaaS platform with 12 Maven modules covering authentication, billing, inventory, GST invoicing, khata credit ledger, and offline sync — entirely as a solo engineer using Spring Boot 3.4 and Java 21, proving that the right monorepo architecture lets one developer ship what most teams need five to ten engineers to build.

The SaaS market is projected to hit $465 billion in 2026. Over 70% of modern SaaS vendors use some form of multi-tenancy. And yet, most developers believe building a multi-tenant SaaS platform requires a team of 5-10 engineers.

It doesn't. It requires the right architecture.

The secret isn't working 16-hour days. It's making architectural decisions that multiply your output instead of dividing it. Here's every decision I made and why.

Why Choose a Monorepo Over Microservices?

This is the most controversial decision. "But microservices scale better!" Sure. They also require:

  • Service discovery (Consul, Eureka)
  • API gateway (Kong, Spring Cloud Gateway)
  • Distributed tracing (Jaeger, Zipkin)
  • Per-service CI/CD pipelines
  • Per-service monitoring and alerting
  • Inter-service communication patterns (REST, gRPC, message queues)
  • Eventual consistency handling

For a solo engineer, microservices multiply complexity by the number of services. 12 microservices = 12x the infrastructure overhead.

A Maven multi-module monorepo gives you:

  • One deployment artifact (single JAR/container)
  • Compile-time dependency validation (if module A breaks module B's API, the build fails)
  • Shared transaction context (billing + inventory in one DB transaction)
  • One CI pipeline, one Docker image, one health check
  • Clean module boundaries that ARE the future microservice boundaries
text
retailos/
├── retailos-common/     # Shared kernel
├── retailos-auth/       # Authentication + RBAC
├── retailos-tenant/     # Tenant management
├── retailos-kyc/        # KYC + compliance
├── retailos-inventory/  # Products + stock
├── retailos-billing/    # POS + payments
├── retailos-invoice/    # GST invoicing
├── retailos-khata/      # Credit ledger
├── retailos-file/       # File storage (MinIO)
├── retailos-sync/       # Offline sync
├── retailos-analytics/  # Dashboard + reports
├── retailos-admin/      # Platform admin
├── retailos-audit/      # Immutable event log
└── retailos-app/        # Entry point

When a module needs independent scaling (e.g., billing handles 10x more traffic than analytics), extraction is straightforward because the interfaces are already clean. You extract when you NEED to, not because the architecture demands it.

How Does Row-Level Tenant Isolation Work at Scale?

Three options for multi-tenancy. The choice determines your cost structure forever.

ApproachIsolationCostComplexity
--------------------------------------
Database-per-tenantMaximum1000 tenants = 1000 DBsHigh
Schema-per-tenantGoodConnection pool per schemaMedium
Row-level (shared DB)AdequateOne DB for all tenantsLow

I chose row-level. Every table has a tenant_id column. Every query is filtered by tenant. Here's why:

Cost math: PostgreSQL on a basic VPS handles thousands of tenants in one database with proper indexing. Database-per-tenant at 1,000 tenants means 1,000 connection pools, 1,000 migration runs, 1,000 backup jobs. Unrealistic for a solo operator.

Enforcement: TenantContext (ThreadLocal) is set by the security filter on every request. It's extracted from the JWT token. Every repository method includes the tenant filter automatically — it's not something developers (me) can accidentally skip.

Indexing strategy: Composite indexes on (tenant_id, <query_column>) keep queries fast. PostgreSQL's query planner handles this efficiently — tenant_id as the leading column means it partitions the index lookup by tenant first.

The trade-off: A bug in the tenant filter could leak data across tenants. Mitigation: integration tests that verify cross-tenant isolation on every CRUD endpoint. One test per module that creates data for Tenant A and asserts Tenant B cannot see it.

Decision #3: India-Specific Modules (Khata + GST)

Two modules that no generic SaaS platform has — and they're the reason RetailOS exists.

Khata (Credit Ledger):

70% of Indian retail involves extending credit. A customer buys ₹500 of groceries and says "khata mein likh do." The shopkeeper records it in a paper diary. Month-end, the customer (hopefully) pays.

Paper khata problems: - Lost diaries = lost money - No running balance visibility - No reminders for overdue accounts - Disputes over amounts with no audit trail

RetailOS khata module: - KhataAccount — One per customer. Running balance, total credit extended, total paid. - KhataEntry — Every credit/debit with timestamp, amount, notes, linked bill. - Auto-linking — When a bill is created with payment type "CREDIT", a KhataEntry is auto-created. Zero double entry. - Settlement flow — Partial or full payments. Balance updates automatically. - WhatsApp reminder — Monthly balance summary sent to customer's WhatsApp (roadmap).

GST Invoice Generation:

Indian GST requires: GSTIN of buyer/seller, HSN codes per item, CGST/SGST/IGST splits based on interstate vs intrastate, sequential invoice numbering, and specific PDF formats.

The invoice module takes a completed Bill, applies GST rules based on HSN codes and buyer/seller states, generates a compliant PDF, and stores it in MinIO. One-click PDF generation from any bill.

Why this matters: Zoho Inventory, Unicommerce, Square — none of them have khata. None handle Indian GST with HSN-level granularity at SME pricing. These two modules ARE the product differentiation.

How Do You Handle Offline Sync with Conflict Resolution?

Internet in Tier 2/3 India is unreliable. A billing counter in a kirana store can't stop working because WiFi dropped for 10 minutes. The point-of-sale must function offline.

The offline sync architecture:

  1. Local operation queue — Every write operation (create bill, update stock, add khata entry) is saved to a local queue
  2. Optimistic execution — The operation executes locally immediately. The UI reflects the change instantly.
  3. Background sync — When connectivity returns, queued operations sync to the server in order
  4. Conflict resolution — Different operation types get different strategies:
Operation TypeConflict StrategyWhy
---------------------------------------
Bill creationAdditive mergeNever lose a sale
Payment recordingAdditive mergeNever lose a payment
Stock movementAdditive mergeStock counts must be accurate
Product metadataLast-write-winsName/price changes are low-risk
Khata entryAdditive mergeNever lose a credit/debit record

Idempotency: Every operation gets a client-generated UUID. If the same operation syncs twice (network retry), the server detects the duplicate and ignores it.

This is the hardest engineering problem in the entire platform. The key insight: for retail, you never lose a transaction. It's better to have a duplicate that gets reconciled than a missing sale that's gone forever.

The Solo Engineer's Multiplier Stack

Building 12 modules alone requires force multipliers. Here's the stack that makes it possible:

ToolMultiplier Effect
------------------------
**Spring Boot 3.4 + Java 21**Convention over configuration. Starters for Security, JPA, Redis, Actuator — each saves 2-3 days of setup
**Flyway**Database migrations as code. Run once for all tenants. No manual SQL scripts
**Swagger/OpenAPI**Auto-generated API docs. Zero documentation effort
**Actuator**Health checks, metrics, environment info — production-ready from day one
**MinIO**Self-hosted S3-compatible storage. No AWS bill. No vendor lock-in
**Docker Compose**Full infrastructure (Postgres + Redis + MinIO) in one command
**JaCoCo**Code coverage reports. Keeps quality visible without a QA team
**Maven multi-module**Compile-time module boundaries. Catch integration bugs at build time

What I deliberately didn't use: - No Kubernetes (overkill for early stage) - No Kafka (PostgreSQL LISTEN/NOTIFY handles event-driven needs for now) - No Elasticsearch (PostgreSQL full-text search is sufficient) - No Redis Cluster (single Redis instance handles the load) - No API Gateway (single application handles routing)

Every technology NOT chosen is time NOT spent on configuration, debugging, and maintenance. The best architecture for a solo engineer is the one with the fewest moving parts that still meets the requirements.

Testing strategy as a solo engineer: Without a QA team, automated tests become your safety net. Each module has integration tests that verify tenant isolation — creating data for Tenant A and asserting Tenant B cannot access it. The billing module has the most extensive test suite because financial calculations cannot have bugs. JaCoCo reports keep coverage visible, and the build fails if critical modules drop below 80% coverage. This discipline is non-negotiable when you're the only engineer — you catch bugs at build time or your users catch them in production.

The lesson: You don't need a team to build a multi-tenant SaaS. You need the right module boundaries, the right tenant isolation strategy, and the discipline to say "not yet" to every technology that doesn't directly serve today's users.

Frequently Asked Questions

Q: Can a solo engineer realistically maintain 12 modules in production?

Yes, because a Maven monorepo deploys as a single artifact. You run one CI pipeline, one Docker container, one health check, and one set of logs. The 12 modules are compile-time boundaries, not runtime boundaries. Monitoring is a single Actuator endpoint, not 12 separate dashboards. The operational overhead is equivalent to maintaining one application, not twelve.

Q: How does row-level tenant isolation prevent data leaks between tenants?

Every request passes through a Spring Security filter that extracts the tenant ID from the JWT token and sets it in a ThreadLocal TenantContext. Every repository query automatically includes a tenant_id filter. Integration tests on every CRUD endpoint verify cross-tenant isolation by creating data for one tenant and asserting another tenant cannot access it. The enforcement is automatic and not something developers can accidentally skip.

Q: Why build a khata module when spreadsheets could handle credit tracking?

Spreadsheets lack integration with billing. When a shopkeeper creates a bill with payment type CREDIT, the khata module auto-creates a ledger entry — zero double entry. It also provides running balances, settlement flows, and an immutable audit trail. Paper khata diaries get lost, have no backup, and create disputes. The digital khata solves all three while being faster than manual entry.

Q: What is the cost of running RetailOS for 1,000 tenants?

With row-level isolation sharing a single PostgreSQL database, the infrastructure cost for 1,000 tenants is a single VPS with PostgreSQL, Redis, and MinIO — approximately $50-100 per month depending on the provider. Database-per-tenant at the same scale would require managing 1,000 connection pools and 1,000 migration runs, which is operationally unrealistic for a solo engineer.

Q: How does the offline sync module handle conflicting edits from multiple billing counters?

Each operation type has a specific conflict strategy. Transaction operations like bill creation, payments, and khata entries use additive merge — both versions are kept because losing a financial transaction is worse than having a duplicate that gets reconciled. Product metadata like names and prices use last-write-wins because these changes are low-risk. Every operation carries a client-generated UUID for idempotency, preventing duplicate syncs from creating duplicate records.

RELATED PROJECT

View Retailos →

Need a founding engineer who builds entire SaaS platforms? That's what I do.

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 →

Rohit Raj — Backend & AI Systems

WhatsAppGitHubLinkedInEmail

Services

Mobile App DevelopmentAI Chatbot DevelopmentFull-Stack Development

Get Updates