Problem
Enterprise operations teams were running equipment, warranty, and claim data across disconnected spreadsheets and legacy single-tenant tools. There was no single source of truth: teams reconciled records by hand, field service requests were routed over phone and email with no SLA tracking, and searching across millions of records was slow and unreliable.
The consequences were concrete — slow service turnaround, revenue leakage on expired warranties, no audit trail for compliance, and zero real-time visibility for leadership.
Business Context
The platform serves industrial equipment and field-service organizations. Its users span very different roles — field engineers, supervisors, call-center agents, commercial managers — each with their own portal, permissions, and workflows. Every customer (tenant) has different approval chains and SLAs, so the system had to treat variability as configuration, not code.
The legacy systems it replaced could not onboard new customers without re-architecture. Multi-tenancy wasn't a feature; it was the business model.
Architecture
The platform is built as event-driven microservices, one per domain (equipment, claims, service, inventory, contracts), communicating over REST for queries and Kafka for state changes.
Edge → API Gateway · Auth Service
Domain → Equipment · Claims · Service · Inventory · Contracts
Platform → Search · Workflow Engine · Notifications
Data → PostgreSQL · Elasticsearch · Redis · Kafka
Three decisions define the architecture:
- Schema-per-tenant isolation. A tenant-resolution filter binds the datasource per request;
the tenant id propagates through every cache key and search query. Isolation is structural,
not a
WHEREclause someone can forget. - Transactional outbox for events. Domain events are written in the same transaction as the business change, then relayed to Kafka. Consumers are idempotent; poison messages go to a dead-letter topic. No dual-write races, no lost events.
- Search as a first-class read model. A custom query DSL is parsed to an AST and compiled to tenant-scoped Elasticsearch queries, with hot facets cached in Redis.
Technology
Java 17 and Spring Boot (Security, Data JPA) on the backend; PostgreSQL as the system of record; Elasticsearch for search; Redis for caching and token blocklists; Kafka for messaging. Deployed on AWS with Docker and Kubernetes, CI/CD through GitHub Actions. Tested with JUnit 5, Mockito, and Testcontainers — integration tests run against real Postgres and Kafka in containers.
Challenges
Tenant-safe search at scale. Relational queries with many optional filters produced unindexed full-table scans and couldn't enforce tenant scope cheaply. I evaluated PostgreSQL full-text search and materialized views before landing on Elasticsearch behind a custom DSL. The trade-off: an indexing pipeline and eventual consistency between the write store and the search index, which the UX had to acknowledge honestly (see the write-up).
Per-tenant workflows without deployments. Approval chains were originally embedded in service code, making every new tenant a code change. A full BPMN engine (Camunda) was the obvious alternative; instead I built a lightweight declarative state machine — states and transitions persisted per tenant, executed by a deterministic engine with guard conditions and side-effect hooks. Less tooling than BPMN, far easier to reason about.
Reliable events under partial failure. Best-effort publishing after commit loses events; two-phase commit was a non-starter operationally. The transactional outbox pattern with idempotent consumers was the answer, and it shaped how every service handles state changes.
Solution
A unified platform of 20+ modules — customer and equipment management, warranty, claims, service, contracts, inventory, warehouse, supplier and parts management, role-specific portals, notifications, the workflow engine, reporting — on one auditable data model. New tenant workflows are configuration, not deployments. Every state change is an event other services can react to.
Results
- p95 search latency under 150ms across 1M+ records, with zero cross-tenant leakage.
- Key query latencies cut ~60% through composite indexes, query tuning, and connection pooling.
- Cache hit rate above 85% on hot read paths after facet caching in Redis.
- 99.9% uptime in production; new tenants onboard without re-architecture.
- Reusable platform modules (tenancy, search, outbox) adopted across product teams.
Lessons Learned
Early investment in clean domain boundaries paid off the most — it made every later decision cheaper. Eventual consistency between the write store and the search index required careful UX handling, not just engineering. And push variability into data: a deterministic engine executing per-tenant configuration beats clever code every time.