Four years ago, I inherited a 90k-line single-file ServiceProvider that brought a Laravel PHP server to its knees every time someone touched the config. The client was a family-owned retail chain in Dubai – lovely people, but their developers had quit halfway through a migration to Laravel 9. I spent three days just untangling service provider logic to even deploy the app without crashing the environment. That disaster forced me to formalize how I structure Laravel apps for real scale.
Modularization Without Overengineering
Rule #1: Keep directories flat unless splitting a module out actually helps development speed. I see too many projects with src/Services/Contracts/Interfaces/Repositories/Abstracts trees that exist to impress nobody.
For a real estate platform (Reach Home Properties), we split the listings engine into its own package. Why? Because real estate agents in the UAE need to import properties via Excel at 2am, and we wanted that logic decoupled from the frontend code that handles booking viewings. We used Laravel’s --pest tests in the package directory and kept the main app’s dependencies lean.
But don’t split packages because "it sounds enterprise." Last year, a client insisted packaging their 300-line PaymentController into a microservice. We wasted four dev days chasing Composer autoloading issues until giving up and collapsing it back into the app/Http/Controllers folder. If your service has fewer than 10 domain-specific classes, don’t package it.
Service Providers Are Your Friends (Even When They’re Not)
I put everything that touches the container in a dedicated AppServiceProvider per domain area. For the Tawasul Limo booking system, this meant separate service providers for:
- •Vehicle availability calculations (with Redis caching)
- •Chauffeur location tracking (WebSocket integration)
- •Multi-currency payment gateways
Laravel’s service providers are like duct tape – ugly but effective. The limo app had a LimoServiceProvider that registered Redis connection hooks during boot. Not glamorous, but it kept the AppServiceProvider from becoming a dumping ground.
Configuration Drift Kills Deployments
Hard truth: If your .env files have 87 entries, you’ve already lost.
For enterprise deployments across UAE banks and logistics companies, I started standardizing this:
# base config
.env # Never committed
.env.production # For deployment pipelines
.env.local # For dev machines
# module-specific
.env.vehicle # Shared between app and CLI processes
.env.paymentsWe used a custom Docker image that enforced this structure, combining .env.production with module-specific configs at runtime. One construction company in Abu Dhabi had a developer who accidentally overrode the AWS SES password using a local .env file – that taught me to separate credentials by environment more strictly.
Localization in the GCC Context
If your app deals with Arabic language support, always test bi-directional layouts in development. I wasted half a day debugging why translated forms in the DAS Holding corporate site displayed text incorrectly until realizing we hadn't set $attributes->merge(['dir' => 'rtl']) in Vue components.
Also, date formatting in the Islamic calendar matters for local clients. Laravel’s built-in Carbon formatting helps, but we ended up writing a custom UaeDates trait for clients who wanted to display both Gregorian and Hijri dates in the same view.
Automated Testing That Doesn’t Waste Time
PestPHP changed the game for us. When the Reach Home Properties codebase grew to 17k lines of Blade templates, PHPUnit tests were taking 12 minutes a run. Switching to Pest’s functional style cut that by 40%. We didn’t rewrite anything – just created Pest.php bootstrapping and gradually converted test files.
For API-heavy work like the Tawasul Limo mobile backend (React Native + Laravel Passport), we used Laravel’s built-in Sanctum testing utilities. Real talk: The documentation for mobile auth token testing is still kinda janky. We hardcoded a X-DEV-API-TOKEN header in test setUp methods just to skip the OAuth2 flow during CI.
Deployment Is Part of Architecture
The limo booking platform got to a point where deployments would crash the site for 30 seconds waiting on config:cache. We switched to Envoyer with zero-downtime builds, but had to write a custom script that warmed Redis caches before switching symlinks. Envoyer isn’t magical – if your queues crash during deployment, it’ll make noise.
One client in the hospitality sector wanted emergency hotfixes at 2am because of last-minute event cancellations. Jenkins pipelines with Git tags became our go-to solution. We’d run php artisan config:clear manually in the pre-deploy hook but only after writing the cached config to a temporary file first.
Laravel doesn’t save you from bad architecture patterns. I’ve worked on 40+ enterprise apps here in Abu Dhabi and still see the same mistakes repeated: over-engineered packages, monolithic service providers, and .env files as long as my CV. What helped me scale applications wasn’t adopting "the next big thing" – it was being ruthless about simplicity.
If you’re drowning in service provider spaghetti or fighting package autoloading, hit me up at sarahprofile.com/contact. I’ll likely send you a screenshot of a Laravel log file from four years ago and say: "Welcome to enterprise PHP development – let’s fix it together."