Saltar al contenido
HA_

Este artículo aún no está disponible en su idioma, por lo que está leyendo el original en inglés.

Context

I joined Southville Solutions in Lahore as a Senior Software Engineer in February 2023, and I still work there.

When I started, a single monolith served every customer. One slow module or one bad deploy touched all tenants at once, and onboarding a new tenant was manual work. I led the decomposition of that monolith into 12 production microservices and architected the multi-tenant infrastructure underneath them.

Constraints

Three things shaped the work:

  • The product had to keep running throughout the decomposition.
  • Every tenant needed hard data isolation. One customer's data could never be reachable from another's.
  • Logs were spread across GCP and AWS. Finding out what went wrong meant looking in more than one place.

Architecture

MonolithGatewayBookingPayroll+10 moreTenant resolver
FIG. 01 / The monolith sits behind a gateway that routes to domain services; every service resolves its tenant's database through a tenant resolver.

Requests go through a gateway to the domain services. Booking and payroll are two of the 12; the rest are drawn as "+10 more". The booking platform is built on NestJS and MongoDB.

Each tenant has its own database. Services reach tenant data through the tenant resolver, which resolves the right tenant database dynamically. New tenant databases are provisioned automatically.

Salary disbursement and attendance run as JMS event pipelines rather than direct calls. Logs from Google Cloud Logging, AWS CloudWatch and Splunk are brought into one observability setup.

Decisions and alternatives I rejected

12 services along domain boundaries, not a rewrite. Because the product had to keep running, replacing the monolith in one release was not an option. Splitting along domain boundaries meant each service could be extracted and deployed on its own, so a bad deploy affects one domain instead of every tenant.

Database-per-tenant, not a shared schema. A shared schema with a tenant column is simpler to run, but isolation then depends on every query filtering correctly. Hard isolation was a requirement, so each tenant gets its own database. The cost is many databases to manage, which is why provisioning is automated and the schema is resolved dynamically. Automated provisioning is also the answer to the manual onboarding problem.

JMS event pipelines for salary disbursement and attendance, not synchronous calls. With events, the service that produces work does not wait on the one that processes it, and a slow consumer does not hold up the rest of the request path.

Central observability, not each cloud's own console. With logs split between GCP and AWS, an incident meant switching tools to follow one request. Bringing Google Cloud Logging, AWS CloudWatch and Splunk together gave one place to detect and trace a problem.

Outcome

  • 1,000+ isolated tenant databases at 99.9% uptime.
  • API response times improved by 40%+ through Redis caching and query work.
  • Incident detection and resolution time reduced by 60%.
  • A booking platform handling 10,000+ daily transactions.

Along the way I mentored 5+ engineers.