Veterinary Clinic ERP - Monolith to Microservices
The Problem
83,000 lines of controller code. Zero automated tests. A single developer leaves and nobody can safely touch what they built. That was the reality of this CodeIgniter 3 monolith serving multiple veterinary clinics across Portugal, each with its own isolated database of client records, animal patients, appointments, invoices, and stock. Every change was a gamble.
I joined to architect and lead the migration to a modern stack, without downtime.
The Solution
A strangler fig migration from CodeIgniter 3 to Laravel 12 microservices, replacing the monolith module by module while both systems run in parallel.
Migration Architecture
An Nginx reverse proxy routes traffic to either the legacy monolith or the new Laravel services based on subdomain. CodeIgniter calls Laravel via cURL over the internal Docker network, forwarding tenant context and session cookies with each request. Once a module is migrated, the microservice becomes the source of truth with no fallback to legacy code.
Each module has its own configurable service URL via environment variables. In development, all point to the same Laravel instance. In production, each can be routed to an independent deployment, enabling fine-grained scaling per module based on actual load.
Multi-Tenancy
Each veterinary clinic has its own isolated MySQL database. A custom middleware dynamically switches the database connection per request based on a tenant header. A single Laravel deployment serves all clinics while maintaining full data isolation. Four named database connections serve different purposes: tenant, shared config, product catalog, and third-party integration.
Authentication Bridge
Rather than building a separate auth system, I designed a session bridge. CodeIgniter stores sessions in Memcached. A custom Laravel middleware reads the legacy session cookie and validates it directly against Memcached, enabling seamless authentication without requiring users to log in twice.
Strict Code Quality
Established and enforced a rigorous baseline from day one:
- Architecture tests: all classes must be
final,declare(strict_types=1)everywhere, no abstract classes, no protected methods, no raw query builder calls - PHPStan Level 5 static analysis via Larastan
- 100% test coverage with Pest PHP parallel execution
- CI pipeline running the full suite (unit tests + lint + types + refactoring) on every pull request
Modules Migrated
- Invoices (most complex): Portuguese fiscal compliance (SAFT import/export), invoice lifecycle, receipt consolidation (from 10+ API calls to a single endpoint), fulltext search with filters
- Products: stock management, write-offs, internal consumption, purchase orders, price update workflows
- Dashboard: real-time visit queues, hospitalization monitoring, visit status management
- Clients: the reference implementation (13 controllers, 14 actions, 8 repositories), CRUD with CRM, account statements, GDPR compliance
Tech Stack
- Legacy: CodeIgniter 3, PHP 8.0
- Microservices: Laravel 12, PHP 8.4
- Database: MariaDB 10.4 (database-per-tenant)
- Sessions: Memcached (shared between legacy and microservices)
- Testing: Pest PHP (parallel, architecture tests), PHPStan Level 5 (Larastan), Rector, Laravel Pint
- Monitoring: Laravel Nightwatch, Sentry
- Storage: AWS S3 (documents, structured logs)
- Infrastructure: Docker (Nginx + PHP-FPM), Bitbucket Pipelines CI/CD
Outcomes
- 525 API endpoints across 6 modules (Dashboard, Clients, Invoices, Products, Listings, Animals)
- 105 actions, 70 repositories, 111 models built with strict Controller/Action/Repository layering
- 103 database migrations and 60 seeders with production-realistic data
- 78 test files with 100% coverage requirement
- 89 database tables per tenant
- Module-based architecture enabling independent deployment and scaling per module