Splitting Orders and Billing with BowPHP Microservices
As an application grows, some responsibilities want to live on their own β their
own deploy cadence, their own scaling, their own team. Billing is a classic
example. In this post we'll extract billing into a separate service and have our
Orders application talk to it using the
bowphp/microservice package over Redis.
We'll use both communication modes the package offers:
- Request/Response (RPC) with
send()β when Orders needs an answer back. - Fire-and-forget (Event) with
emit()β when Orders just wants to announce that something happened.
The scenarioβ
Two applications:
- Orders β the customer-facing app. When a customer checks out, it needs an
invoice total now (RPC), and it wants to announce
order.placedso other services can react asynchronously (event). - Billing β owns invoices. It exposes an
invoice.createRPC handler and listens fororder.placedevents.
Both share a Redis broker.
Step 1 β Install the package (both apps)β
composer require bowphp/microservice
Register the provider in each application's kernel:
public function configurations(): array
{
return [
\Bow\Microservice\Bow\MicroserviceConfiguration::class,
];
}
Then publish the config file:
php bow microservice:publish-config
Step 2 β Point both apps at Redisβ
In config/microservice.php, select the Redis transport and the broker
coordinates. This is identical on both sides:
return [
'transport' => 'redis',
'timeout' => 3.0,
'controllers' => [
// Billing registers its consumer here (see Step 4)
],
'redis' => [
'host' => app_env('MICROSERVICE_HOST', 'redis.internal'),
'port' => (int) app_env('MICROSERVICE_PORT', 6379),
'password' => app_env('MICROSERVICE_REDIS_PASSWORD', null),
],
];
Step 3 β The Orders side (the client)β
The provider binds a shared ClientProxy in the container. Inject it by type and
you're ready to talk to Billing.
namespace App\Controllers;
use Bow\Microservice\Client\ClientProxy;
class CheckoutController
{
public function __construct(private ClientProxy $client) {}
public function store(int $orderId)
{
// 1. RPC: ask Billing to create the invoice and wait for the total.
$invoice = $this->client->send('invoice.create', [
'order_id' => $orderId,
'lines' => $this->orderLines($orderId),
]);
// 2. Event: announce the order; we don't wait for anyone.
$this->client->emit('order.placed', [
'order_id' => $orderId,
'total' => $invoice['total'],
]);
return response()->json([
'invoice_id' => $invoice['id'],
'total' => $invoice['total'],
]);
}
}
send() blocks until Billing replies (or the timeout fires); emit() returns
immediately and never waits.
The default timeout comes from config('microservice.timeout'), but a single
heavy call can override it:
$report = $this->client->send('billing.report', $payload, timeout: 30.0);
emit() is fire-and-forget, so it does not accept a timeout.
Step 4 β The Billing side (the server)β
Generate a consumer:
php bow add:consumer InvoiceConsumer
Annotate handlers with #[MessagePattern] for RPC and #[EventPattern] for
events. The method name doesn't matter β the pattern is what routes the
message:
namespace App\Consumers;
use App\Models\Invoice;
use Bow\Microservice\Consumer\MessagePattern;
use Bow\Microservice\Consumer\EventPattern;
class InvoiceConsumer
{
/**
* RPC: create an invoice and return it to the caller.
*/
#[MessagePattern('invoice.create')]
public function create(array $payload): array
{
$invoice = Invoice::create([
'order_id' => $payload['order_id'],
'total' => $this->sum($payload['lines']),
]);
$invoice->persist();
return [
'id' => $invoice->id,
'total' => $invoice->total,
];
}
/**
* Event: react to a placed order β no response returned.
*/
#[EventPattern('order.placed')]
public function onOrderPlaced(array $payload): void
{
// e.g. queue a receipt email, update analytics, etc.
}
private function sum(array $lines): float
{
return array_sum(array_column($lines, 'amount'));
}
}
Register the consumer in Billing's config/microservice.php:
'controllers' => [
\App\Consumers\InvoiceConsumer::class,
],
Step 5 β Run the Billing listenerβ
Billing runs a long-lived process that subscribes to the broker and dispatches incoming messages to your handlers:
php bow microservice:listen
Need to override the configuration for a one-off process? The command takes flags:
php bow microservice:listen \
--transport=redis \
--controllers="App\Consumers\InvoiceConsumer" \
--patterns="invoice.create,order.placed"
How the pieces fitβ
Customer checkout
β
βΌ
Orders ββsend('invoice.create')βββΆ Billing (RPC, waits for the total)
βββββββββ invoice βββββββββ
β
ββemit('order.placed')βββββΆ Billing (event, fire-and-forget)
+ any other subscriber
Why this mattersβ
- Independent deploys β Billing can ship on its own schedule.
- Right tool per call β synchronous
send()where you need an answer, asynchronousemit()where you don't. - Swap the transport, not the code β start on Redis; move to RabbitMQ or
Kafka later by changing
config/microservice.php, with your handlers untouched.
When one service needs to scale, fail, or deploy independently of the rest, this is how you draw the line β without leaving the BowPHP toolset. The full microservice documentation covers the other transports, multiple clients, and per-process tuning.
Is something missing?
If you run into problems with the documentation or have suggestions to improve the documentation or the project in general, please open an issue for us, or send a tweet mentioning the Twitter account @bowframework or directly on github.