Introduction
Microservices have become a staple of modern web development for a few reasons — scalability, separation of concerns, independent deployment, and more. But if you’re a Laravel developer, you might assume you need to reach for something like Go or Node.js to get started.
You don’t.
Laravel has everything you need to build small, focused services that talk to each other cleanly — and it comes with the tooling, testability, and developer experience you already know and love.
In this post, I’ll walk you through the exact approach I used to build two Laravel apps that communicate via API — one acting as a microservice that receives webhooks, processes data, and forwards it to another Laravel app that stores and displays the results. It’s the kind of real-world setup you’ll find in modern SaaS platforms and large-scale systems.
Why Laravel for Microservices?
The name microservice can be misleading. It gives the impression that you need an alternative language to PHP. But microservices aren’t about language choice - they’re about separation of concerns.
A microservice is just a small, focused application that does one thing well and communicates over HTTP (or another protocol). That’s it.
And in reality many companies are using PHP - and Laravel in particular - to build microservices. It’s quick to scaffold, has a rich ecosystem, and includes everything you need out of the box:
- Simple routing with clean controllers
- Built-in validation, jobs, events, queues, and service providers
- First-class test support (fakes, HTTP testing, etc.)
- Great developer experience and tooling with Artisan
If you already know Laravel, you're much closer to building microservices than you think.
⚙️ Microservice Responsibility
Let’s zoom out for a second and talk about what we’re actually building.
Modern mobile apps often offer premium features through in-app subscriptions. When users subscribe, cancel, renew, or perform other billing-related actions, platforms like Apple iOS and Google Android publish webhooks — small, real-time payloads of data that describe what just happened.
Our Laravel microservice is built to subscribe to these webhooks. It acts as a middle layer between Apple/Google and a central CRM-like platform - a system we call AudienceGrid. The job of the microservice is to take those raw webhook payloads, process and format the data, and forward it onto AudienceGrid in a clean, structured way.
This architecture gives us a clear separation of responsibilities:
- Apple/Google 👉 send the raw webhook data
- Laravel microservice 👉 validate, transform, and forward it
- AudienceGrid 👉 receive, store, consolidate, and display the data
The microservice doesn’t render views or manage sessions. It’s an API-only, stateless service built to handle incoming events efficiently and reliably. That’s why our routing is so simple. We register a single webhook endpoint:
1Route::post('/webhook', WebhookController::class);
And our controller is a single-action controller using Laravel’s __invoke
method:
1public function __invoke(Request $request): JsonResponse2{3 // Handle the incoming webhook4}
This setup mirrors what you’ll find in many production environments. A single, focused endpoint receives data from external systems - and your logic sits cleanly inside the controller, supported by Laravel’s built-in validation, DTOs, and HTTP helpers.
🤹 Handling Incoming Webhooks
Once our microservice receives data from Apple or Google, the next step is to process it. But not all webhooks are created equal.
A webhook might represent:
- A new subscription
- A renewal
- A cancellation
- Or any number of user events coming from iOS or Android apps
To deal with this, our microservice needs to identify two things:
- The source (Apple or Google)
- The type of event (start, renewal, cancel, etc.)
Only then can we determine how to handle the data.
To keep this logic clean, scalable, and easy to extend, we use a delegation pattern. Instead of a big controller or a giant switch statement, we built a HandlerDelegator
that loops over a collection of tagged services — each responsible for handling one kind of webhook.
Here’s the key part of the delegator:
1foreach ($this->handlers as $handler) {2 if ($handler->supports($webhook)) {3 $handler->handle($webhook);4 }5}
Each handler implements a WebhookHandler
interface with two methods:
-
supports(Webhook $webhook): bool
-
handle(Webhook $webhook): void
This approach allows us to register handlers for different platforms or events. Each handler checks if it supports the webhook, and if so, it takes over. This makes the system open to extension but closed to modification — a clean example of the Open/Closed Principle.
And here’s where things get even more interesting: we tag our handlers in the service container so that Laravel can lazily load them when the delegator runs:
app/Providers/AppServiceProvider.php
1$this->app->tag([2 AppleWebhookHandler::class,3 GoogleWebhookHandler::class,4], 'webhook.handler');
Then we resolve them using:
app/Providers/AppServiceProvider.php
1$this->app->bind(HandlerDelegator::class, function ($app) {2 return new HandlerDelegator($app->tagged('webhook.handler'));3});
Laravel’s container supports tagging out of the box — and it’s a great pattern when you want to build systems that are modular, efficient, and easy to grow. You can read more about tagging in the Laravel docs.
This style of delegation is something I’ve seen more commonly in Symfony applications, but it fits beautifully into Laravel’s architecture. That’s the beauty of working across ecosystems — you get to bring battle-tested ideas from one world into another.
🤙 Talking to Other Services
A huge part of microservices architecture is communication. Services don’t live in isolation — they send data to each other constantly. In our case, once the microservice receives and processes a webhook, it needs to forward that data to another Laravel app (AudienceGrid) for storage, consolidation and display.
Laravel makes this easy with its built-in HTTP client.
Under the hood, it’s a thin wrapper around Guzzle — but with cleaner syntax, smarter defaults, and most importantly, fantastic support for testing. In my own projects, I like to wrap the HTTP facade in a dedicated client class. This makes it easier to manage dependencies as well as offering a little extra flexibility when it comes to testing. Here’s an example of what that looks like:
1class AudienceGridClient2{3 public function sendEvent(array $payload): void4 {5 Http::post(config('services.audiencegrid.url') . '/events, $payload);6 }7}
But what really makes Laravel’s HTTP client stand out is its test API.
When you’re building microservices, you’re often more concerned with verifying that data was forwarded correctly, rather than checking the response. You care that the event got to the next system with the expected structure and timing.
Laravel gives you a dead-simple way to test that:
1Http::fake();2Http::assertSentCount(1);3Http::assertSent(function (Request $request) {4 return $request->url() === 'https://audiencegrid.test/track' &&5 $request['event'] === 'subscription_started';6 7 // other assertions8});
That’s it — no mocking libraries, no complex container setup. Just clear assertions that prove your service is doing its job.
Laravel’s HTTP client isn’t just convenient — it’s microservice-friendly by design.
🎒 Lessons Learned
Microservices come with a lot of benefits: they’re modular, scalable, and well-suited to distributed systems. But they also come with trade-offs — especially around complexity.
When you split an application into separate services, things will fail: network issues, malformed payloads, external APIs going down, or data not arriving in the order you expected.
That’s why one of the biggest lessons from building microservices is this: you need solid error handling and visibility into what’s going on — especially in production.
In the course, we dive into this head-on. We talk about what makes error handling in a microservice environment different, and how to implement custom error handlers for different environments. For example, in development you want full stack traces and debug info. In production? You want structured logging, alerts, and silence when needed.
Here’s how we handle this in the Laravel Microservice course:
1$this->app->singleton(ErrorHandler::class, function () {2 if (app()->isProduction()) {3 return new AppErrorHandler();4 }5 6 return new DebugErrorHandler();7});
This makes it easy to plug in different behavior depending on where the app is running — and to keep your logs clean, useful, and production-safe.
We also touch on:
- Strategies for catching unexpected failures (like malformed webhooks)
- How to simulate failure scenarios in your tests
- Why monitoring and observability are non-negotiable when services are decoupled
These challenges are exactly why some developers criticize microservice architectures — and they’re not wrong. It’s more moving parts. But with the right tooling and patterns (and Laravel gives you a lot out of the box), you can build services that are resilient, testable, and production-ready.
Thanks For Reading 🙏
Thanks for reading to the end! I hope this gives you a clearer view of how Laravel can be used effectively in a microservices architecture, and what kind of real-world decisions and patterns are involved.
I’ve built similar services in other frameworks and languages, but Laravel’s developer experience was so painless and quick! So many of the hard problems are already solved for you — from routing and validation to testing and error handling — which makes it a great fit for building focused, reliable microservices.
If you want to go deeper, this setup is part of a full video course I created: Check it out here: Laravel Microservice
There’s also a bundle with Battle Ready Laravel by Ash Allen, and together they cover both building robust services and understanding modern PHP architecture from the ground up. Check it out here: Battle Ready Laravel Microservice
Make sure you use the coupon ASH_ALLEN25 for a 25% discount if you wish to enroll!
Got questions about Laravel microservices? Ideas, feedback, or something you’re working on that’s similar? I’d love to hear about it — feel free to reach out on Twitter or LinkedIn.
Big thanks to Ash for letting me share this on his blog — his work has helped countless Laravel devs (me included), and it’s great to be able to contribute here.