Skip to content

Build an HTTP write workflow

The reference app's POST /api/client/orders validates at the HTTP boundary and enters PlaceOrder once. The UseCase owns the transaction; OrderResource owns the JSON shape.

This workflow uses a Controller, DTO, Action, Event, Query, Domain, and Domain Exception.

Before you begin

Run PULSAR_CORE_PATH=../../../core composer run reference:reset from examples/reference-app, then read the inbound adapter rule and transaction ownership.

Build the write path

StoreOrderRequest authorizes an authenticated client and validates the input. The Controller converts only validated data to a Domain DTO, calls one UseCase, and shapes the delivery-neutral result.

examples/reference-app/app/Pulsar/Services/Client/Modules/Orders/Controllers/OrderController.php — HTTP boundary calls one UseCase.

php
<?php

namespace App\Pulsar\Services\Client\Modules\Orders\Controllers;

use App\Pulsar\Domain\Orders\DTOs\CreateOrderData;
use App\Pulsar\Domain\Orders\Models\Order;
use App\Pulsar\Services\Client\Modules\Orders\Requests\StoreOrderRequest;
use App\Pulsar\Services\Client\Modules\Orders\Resources\OrderResource;
use App\Pulsar\Services\Client\Modules\Orders\UseCases\PlaceOrder;
use App\Pulsar\Services\Client\Modules\Orders\UseCases\ViewOrder;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Gate;

final class OrderController
{
    // #region order-store-controller
    public function store(StoreOrderRequest $request, PlaceOrder $useCase): JsonResponse
    {
        $order = $useCase->execute(new CreateOrderData(
            tenantId: $request->user()->tenant_id,
            reference: $request->string('reference')->toString(),
            amountCents: $request->integer('amount_cents'),
        ));

        return (new OrderResource($order))->response()->setStatusCode(201);
    }
    // #endregion

    // #region order-show-controller
    public function show(Order $order, ViewOrder $useCase): OrderResource
    {
        Gate::authorize('view', $order);

        return new OrderResource($useCase->execute($order->id));
    }
    // #endregion
}

examples/reference-app/app/Pulsar/Services/Client/Modules/Orders/UseCases/PlaceOrder.php — Transaction and after-commit Event ownership.

php
<?php

namespace App\Pulsar\Services\Client\Modules\Orders\UseCases;

use App\Pulsar\Domain\Orders\Actions\CreateOrder;
use App\Pulsar\Domain\Orders\DTOs\CreateOrderData;
use App\Pulsar\Domain\Orders\Events\OrderPlaced;
use App\Pulsar\Domain\Orders\Models\Order;
use Illuminate\Support\Facades\DB;

final class PlaceOrder
{
    public function __construct(private readonly CreateOrder $createOrder) {}

    // #region place-order-workflow
    public function execute(CreateOrderData $data): Order
    {
        return DB::transaction(function () use ($data): Order {
            $order = $this->createOrder->execute($data);
            DB::table('outbox_messages')->insert([
                'event_type' => 'orders.placed.v1',
                'aggregate_id' => (string) $order->id,
                'payload' => json_encode(['order_id' => $order->id, 'version' => OrderPlaced::VERSION], JSON_THROW_ON_ERROR),
                'idempotency_key' => 'order-placed-'.$order->id,
                'created_at' => now(),
                'updated_at' => now(),
            ]);
            event(new OrderPlaced($order->id));

            return $order;
        });
    }
    // #endregion
}
A client request enters Laravel, validates through StoreOrderRequest, reaches one OrderController and one PlaceOrder UseCase, persists an Order through CreateOrder inside the transaction, and returns shaped JSON through OrderResource.
Place an order through the HTTP write workflow. The Client Service validates at the boundary, while PlaceOrder owns the transaction and OrderResource shapes the response.

Interpretation. Validation and response assembly stay outside the workflow; the Action persists one aggregate inside the UseCase transaction.

Verify failures and success

Run composer run reference:test:http. The group proves unauthenticated rejection, validation errors, a 201 response, the JSON response fields, and an authorization denial. A duplicate reference fails inside the transaction and rolls the write back; map a Domain Exception in bootstrap/app.php, never in a Domain type.

Troubleshoot

If a Controller reaches a Query or Action, move that call into the UseCase. Do not return an HTTP response from a Domain type.

Continue with read-only endpoints or review Request and Resource.