Services
A Service is the class that implements a Contract. Its only job is to translate Contract method calls into Action calls. The Service holds no business logic and runs no database queries — every query, every write, every transaction lives inside an Action.
A Service lives at src/Domain/{Domain}/Services/{Entity}Service.php. It's bound to its Contract in the domain's ServiceProvider, so callers always resolve the Contract — app(OrderContract::class) — and the framework hands them the Service.
The shape
namespace Domain\Order\Services;
use Domain\Order\Actions\CreateOrder;
use Domain\Order\Actions\GetOrder;
use Domain\Order\Actions\GetOrders;
use Domain\Order\Actions\UpdateOrder;
use Domain\Order\Contracts\OrderContract;
use Domain\Order\DataTransferObjects\CreateOrderData;
use Domain\Order\DataTransferObjects\FilterOrderData;
use Domain\Order\DataTransferObjects\UpdateOrderData;
use Domain\Order\Models\Entities\Order;
class OrderService implements OrderContract
{
public function getOrders(?FilterOrderData $data, array $withRelations = [], array $withCount = []): GetOrders
{
return GetOrders::for($data, $withRelations, $withCount);
}
public function getOrder(int $id): ?Order
{
return GetOrder::handle($id);
}
public function create(CreateOrderData $data): Order
{
return CreateOrder::handle($data);
}
public function update(Order $entity, UpdateOrderData $data): Order
{
return UpdateOrder::handle($entity, $data);
}
}
Each method matches the Contract signature exactly — same parameters, same defaults, same return type — and does nothing except call an Action.
Services delegate to Actions — no direct queries
A Service method contains zero direct Eloquent queries. Every database touch — Model::query(), Model::find(), Model::create(), ->update(), ->delete(), DB::table(...), raw SQL — lives in an Action. The Service orchestrates; the Action executes.
// ❌ Bad — Service holds the query
class EmployeeService implements EmployeeContract
{
public function getEmployee(int $id): ?Employee
{
return Employee::query()->where('id', $id)->first();
}
}
// ✅ Good — Service delegates
class EmployeeService implements EmployeeContract
{
public function getEmployee(int $id): ?Employee
{
return GetEmployee::handle($id);
}
}
final class GetEmployee
{
public static function handle(int $id): ?Employee
{
return Employee::query()->where('id', $id)->first();
}
}
The same rule applies to writes — wrap the write in an Action with DB::transaction() rather than inlining it in the Service:
// ❌ Bad — write logic in the Service
class OrderService implements OrderContract
{
public function create(CreateOrderData $data): Order
{
return DB::transaction(function () use ($data) {
$order = Order::create([...]);
$order->lines()->createMany([...]);
return $order;
});
}
}
// ✅ Good — Service delegates, transaction lives in the Action
class OrderService implements OrderContract
{
public function create(CreateOrderData $data): Order
{
return CreateOrder::handle($data);
}
}
Actions are reusable across multiple Services, easy to test in isolation, and keep query logic centralised. Services become thin orchestrators.
Orchestration belongs in an Action, not the Service
If a Service method needs cross-domain orchestration — say create calls ProviderContract and AccountingContract — put that orchestration in an Action and have the Service delegate to it. The Service stays a pass-through:
// ✅ Good — orchestration in the Action, Service is a pass-through
class OrderService implements OrderContract
{
public function create(CreateOrderData $data): Order
{
return CreateOrder::handle($data);
}
}
final class CreateOrder
{
public static function handle(CreateOrderData $data): Order
{
return DB::transaction(function () use ($data) {
$provider = app(ProviderContract::class)->getProvider($data->provider_id);
$order = Order::create([
'provider_id' => $provider->id,
'customer_id' => $data->customer_id,
'amount' => $data->amount,
]);
app(AccountingContract::class)->recordTransaction($order->id, $data->amount);
return $order;
});
}
}
Bind the Contract in a ServiceProvider
Each domain has one ServiceProvider in src/Domain/{Domain}/Providers/{Domain}ServiceProvider.php. Use the $bindings array — no manual $this->app->bind() calls. Laravel reads $bindings at boot and registers each pair.
namespace Domain\Order\Providers;
use Domain\Order\Contracts\OrderContract;
use Domain\Order\Services\OrderService;
use Illuminate\Support\ServiceProvider;
class OrderServiceProvider extends ServiceProvider
{
public array $bindings = [
OrderContract::class => OrderService::class,
];
}
Then register the provider in bootstrap/providers.php:
return [
// ...
Domain\Order\Providers\OrderServiceProvider::class,
];
If the domain has multiple Services (e.g., Order has OrderService and OrderLineService), bind each pair in the same ServiceProvider:
public array $bindings = [
OrderContract::class => OrderService::class,
OrderLineContract::class => OrderLineService::class,
];
bootstrap/providers.php is the single most common cause of BindingResolutionException: Target [Domain\X\Contracts\YContract] is not instantiable. When a freshly written Contract throws that error, check bootstrap/providers.php first.Resolve dependencies inline inside the domain
Inside domain classes (Services and Actions) we resolve dependencies inline with app(SomeContract::class), not via constructor injection. This contradicts the generic Laravel guidance — the override is deliberate.
// ❌ Bad — constructor injection inside a domain Service
class OrderService implements OrderContract
{
public function __construct(
private ProviderContract $providerService,
private AccountingContract $accountingService,
) {}
public function processOrder(int $id): void
{
$provider = $this->providerService->getProvider($id);
// ...
}
}
// ✅ Good — resolve inline
class OrderService implements OrderContract
{
public function processOrder(int $id): void
{
$provider = app(ProviderContract::class)->getProvider($id);
$accounting = app(AccountingContract::class);
// ...
}
}
Three reasons:
- Actions are static and have no constructor — they must use
app()anyway. Making Services match keeps the pattern consistent end-to-end. - Cross-domain dependency lists in Services balloon to 8–15 Contracts. Constructor injection clutters the signature for dependencies that are only used in one or two methods.
- The rest of the codebase already follows this pattern. New code that uses constructor injection in a Service stands out and breaks the
app()-based test stubs other modules rely on.
Constructor injection remains correct for non-domain classes:
- Controllers — inject the domain Contract via the constructor (see Controllers).
- Form Requests, Middleware, Console Commands, Jobs — standard Laravel constructor injection.
- Adapter classes at system boundaries — payment gateways, notification channels, HTTP clients.
The override applies to Services and Actions inside src/Domain/. Everywhere else, follow the generic Laravel rule.
Cross-domain calls go through the Contract
Inside a domain, you own the code and may import Actions directly. Across domains, go through the upstream Contract.
// ✅ Same domain — Action is yours
use Domain\Order\Actions\CalculateOrderTotal;
$total = CalculateOrderTotal::handle($order);
// ❌ Cross-domain — Action is not yours
$provider = \Domain\Provider\Actions\GetProvider::handle($id);
// ✅ Cross-domain — go through the Contract
$provider = app(ProviderContract::class)->getProvider($id);
See Domain Boundaries for the full diagram and rationale.
Anti-patterns to avoid
- Direct model query inside a Service method —
Model::query(),Model::find(),->create(),->update()are all forbidden in Services. Move to an Action. (anti-patterns #15) - Calling another domain's Service or Action directly — every cross-domain call goes through the Contract. No
use Domain\Other\Services\...oruse Domain\Other\Actions\...outside that domain's own files. (anti-patterns #1) - Constructor injection in a Service — use
app(SomeContract::class)inline. (anti-patterns #7) -
DB::transaction()inlined in a Service — wrap the work in an Action; the Service stays a pass-through.
What to read next
- Contracts — the interface every Service implements.
- Actions — where the queries and writes actually live.
- DTOs — typed payloads between Services and the outside world.
- Domain Boundaries — the rule for who can call whom.
Actions
How to write Actions — the smallest reusable unit of business logic in the codebase — and the rules that keep them consistent and safe.
Contracts
The PHP interface that defines a domain's public API. Everything outside the domain talks to it through the Contract — never through a Model, Action, or Service class directly.