Business Problem
The company sold property management software to real estate agencies, but the original codebase was a nine-year-old PHP monolith with a single shared database. Every new agency added load to the same MySQL instance, and one tenant running a large end-of-month statement batch could degrade response times for everyone. Support tickets about slowness spiked to 60+ per week during peak periods.
Sales had signed two enterprise agencies with 5,000+ managed properties each, and the platform simply could not onboard them safely. Deployments were monthly, all-hands-on-deck events with a rollback rate around 20%. The business needed a platform that could scale per tenant, ship changes weekly or faster, and pass the security reviews these larger customers required.
Architecture
I designed a strangler-fig migration from the monolith to domain-aligned Laravel services: tenancy and identity, property and lease management, trust accounting, and communications. Each service owned its schema, with tenant isolation enforced through a shared tenancy library that scoped every query by tenant ID and blocked cross-tenant access at the ORM layer. An API gateway handled routing, rate limiting per tenant, and JWT validation.
Cross-service workflows such as lease renewal and rent disbursement ran through Redis-backed queues rather than synchronous calls, so a slow accounting job never blocked the property service. The heaviest workload, end-of-month statement generation for thousands of owners per agency, moved to a dedicated worker fleet on AWS that autoscaled from 2 to 24 containers based on queue depth.
The Vue.js frontend consumed a versioned REST API, which let us migrate screens off the legacy monolith one module at a time without a big-bang cutover. Stripe handled subscription billing with per-tenant metered usage for SMS and document storage.
System Design
Tenant-scoped query builder baked into a shared Laravel package, making cross-tenant data leaks a compile-time-style impossibility rather than a code-review hope.
Read replicas for reporting queries, with a query router that sent anything tagged as analytical to the replica pool, cutting primary DB load by roughly 40%.
Idempotency keys on all financial mutations so retried disbursement jobs could never double-pay an owner.
Queue-depth-based autoscaling for statement workers, sized so month-end runs completed inside a 4-hour window instead of the previous 14 hours.
Per-tenant rate limits and circuit breakers at the gateway, so a single agency running a bulk import could not starve other tenants.
Blue-green deployments via GitHub Actions with automated smoke tests against a seeded tenant before traffic switch.
My Responsibilities
Owned the target architecture and the 18-month strangler migration roadmap, sequenced so revenue-critical trust accounting moved last.
Designed and built the multi-tenancy library used by every service, including the tenant-scoped ORM layer and connection routing.
Led a team of five engineers through the extraction of the property and communications domains.
Rebuilt the end-of-month statement pipeline as an autoscaling queue-driven system on AWS ECS.
Introduced the CI/CD pipeline on GitHub Actions, taking deployments from monthly manual releases to daily automated ones.
Ran architecture reviews and wrote the security documentation that got the platform through two enterprise procurement audits.
Implementation
The tenancy package came first: a Laravel service provider that resolved the tenant from the JWT, bound it into the container, and applied a global query scope to every Eloquent model. It also managed per-tenant Redis prefixes and S3 paths. Getting this right early meant every subsequently extracted service inherited correct isolation for free.
Service extraction followed a repeatable playbook: define the API contract, build the service against a copy of production data, dual-run with shadow traffic for two weeks, compare responses, then flip the gateway route. The communications service went first as the lowest-risk domain, and the lessons from it cut the property service migration time roughly in half.
Observability was non-negotiable given the moving parts. Structured logs with tenant and request IDs flowed to CloudWatch, and I added latency and error-rate dashboards per service with alerts wired to on-call. The reconciliation tooling for accounting doubled as a permanent data-quality monitor after cutover.
Results
Onboarded both 5,000+ property enterprise agencies; the larger one imported 480k historical records in under 3 hours using the bulk lanes.
p95 API latency dropped from 1.4 s to 180 ms across the top 20 endpoints.
End-of-month statement generation went from a 14-hour overnight ordeal to a 3.5-hour autoscaled run with zero manual intervention.
Deployment frequency moved from monthly to daily, with change failure rate falling from ~20% to under 3%.
Platform sustained 99.95% measured uptime over the following 12 months against a 99.9% contractual SLA.
Support tickets attributable to performance dropped from 60+ per week to single digits.
Lessons Learned
Extract the lowest-risk domain first, even if it is not the most painful one; the migration playbook you build is worth more than the early win itself.
Dual-write plus automated reconciliation buys you the confidence to migrate money-handling systems that no amount of testing alone can provide.
Multi-tenancy enforced in a shared library beats multi-tenancy enforced by convention every single time.
Per-tenant rate limiting is a product feature, not just an infrastructure control; it changed how sales scoped enterprise deals.
Future Improvements
Move the largest tenants to dedicated database shards with an automated shard-rebalancing tool.
Replace the remaining monolith cron jobs with event-driven schedules to retire the legacy box entirely.
Adopt OpenTelemetry for distributed tracing across the service boundary instead of correlating logs by request ID.
Introduce a customer-facing status page fed directly from the same SLO metrics used internally.
Challenges
Migrating trust accounting without downtime
Trust accounting handles client money and is regulated, so we could not tolerate inconsistency during migration. I built a dual-write layer with nightly reconciliation reports comparing ledgers between old and new systems; we ran both in parallel for six weeks and only cut over after 30 consecutive days of zero-diff reconciliation.
Noisy-neighbor tenants
Two agencies represented 30% of total data volume. Per-tenant rate limits at the gateway plus dedicated queue lanes for bulk operations meant their imports and reports ran on isolated capacity, and p95 latency for smaller tenants stopped correlating with big-tenant activity.
Schema drift during the strangler period
For 18 months, some tables were owned by the monolith and read by new services. I enforced a rule that only one system could write any given table, published change-data events for readers, and tracked ownership in a living schema registry document that gated migrations in CI.