Building a Multi-District Government System with Laravel Microservices
At Orange Business Development I worked on the Beneficiary Training Management System for RDCD, under Bangladesh's Ministry of Local Government, a platform that manages training programs for 50,000+ beneficiaries across multiple districts. This post covers the architecture decisions we made, why we split the system into services, and what I'd do differently.
The problem
A beneficiary's journey runs through several distinct workflows: registration and verification, training batch allocation, attendance, allowance disbursement, and reporting up the administrative hierarchy (union → upazila → district → ministry). Each workflow has different load patterns and different stakeholders. Registration spikes when a new program opens; reporting spikes at month-end; disbursement must integrate with a payment gateway and absolutely cannot double-pay.
Why microservices, and why "boring" ones
We didn't start from a microservices ideology. We started from three operational facts:
- The payment/disbursement flow needed stricter change control and isolation than the rest of the system: a bug in reporting must never take down payments.
- District-level usage was bursty, and we wanted to scale the public-facing registration service independently of the internal admin tools.
- Multiple teams worked in parallel, and a modular monolith was already showing merge-conflict and deploy-coupling pain.
So we split along those seams: a registration/beneficiary service, a training-management service, a disbursement service wrapping the payment gateway, and a notification service (SMS and in-app, since many beneficiaries are reachable only by phone). Each service is a plain Laravel application with its own MySQL schema, fronted by REST APIs, with no exotic infrastructure, because a government project's operations team has to be able to run it for years.
Decisions that paid off
- Database-per-service, reporting via read replicas. Cross-service joins were the biggest temptation. Instead, the reporting service consumed read-only replicas and materialized nightly aggregates, which kept month-end reporting load away from transactional databases.
- Idempotency keys on disbursement. Every payment request carries a client-generated idempotency key, stored with a unique constraint. Gateway timeouts and retries were common; duplicate payments were not.
- API performance work where it mattered. Profiling showed most latency came from a handful of N+1 query patterns and unindexed filters on district/batch lookups. Fixing those (eager loading, composite indexes, and response caching on slow-changing reference data) improved processing speed by roughly 35% across the 30+ endpoints we built and optimized.
- CI/CD from day one. GitHub Actions pipelines per service (test, build, deploy) cut our deployment time by about 60% compared with the manual process the project started with, and made multi-service releases predictable. (I wrote up the pipeline details in a separate post.)
Lessons learned
- Service boundaries are organizational, not just technical. Our split worked because it matched who changed what. A split that crosses one team's daily workflow just adds network calls to their day.
- Government scale is about spikes and audits, not steady throughput. Design for the enrollment-deadline day and the auditor's CSV export, not the average Tuesday.
- Keep the number of services small. Four services with clear contracts beat twelve with fuzzy ones. Every additional service is another deploy pipeline, another log stream, another thing the operations handover document must explain.
- Notifications deserve their own service earlier than you think. SMS gateways fail independently of everything else; isolating that failure mode saved us repeatedly.
The system is live at service.rdcd.gov.bd, serving beneficiaries across multiple districts. If you're building something similar, especially in a public-sector context, I'm happy to compare notes; reach me through the contact page.