Should integration logic live inside the store or in a separate Laravel service?
Integration logic belongs in a separate Laravel service once it touches an external API: authentication, retries, response mapping, or business rules tied to that outside system. Simple local persistence can stay in the store action itself.
Quick Answer
Keep simple persistence in the store action or controller. Move substantial integration logic into a dedicated service instead. A good boundary: the store action handles the incoming request. The integration service handles communication with the external system. Reach for a separate service once you’re authenticating, retrying, or mapping data from an outside system, not for a plain Eloquent create.
What the store action should handle
A store action, such as a controller’s store() method, should coordinate the operation. It receives validated input, calls the appropriate service, and returns a response.
public function store(StoreOrderRequest $request, OrderIntegrationService $service)
{
$order = $service->create($request->validated());
return redirect()->route('orders.show', $order);
}The controller coordinates the operation. It should not need to know how the external integration actually works.
What belongs in the integration service
A dedicated service should own the logic specific to the external system:
- Building the external API request and adding authentication or headers
- Calling the external API or SDK
- Handling external responses and applying retry behavior
- Translating external errors into application-level exceptions
- Mapping external data into your application’s own structures
- Coordinating several external calls that form one application operation
Laravel’s HTTP client covers authentication, timeouts, retries, and error inspection directly, which makes it a natural fit inside this kind of service:
namespace App\Services;
use App\Models\Order;
use Illuminate\Support\Facades\Http;
class OrderIntegrationService
{
public function create(array $data): Order
{
$response = Http::timeout(10)
->retry(3, 100)
->withToken(config('services.example.token'))
->post('https://example.com/api/orders', [
'reference' => $data['reference'],
'amount' => $data['amount'],
]);
if ($response->failed()) {
throw new \RuntimeException('The external order integration failed.');
}
return Order::create([
'reference' => $data['reference'],
'external_id' => $response->json('id'),
'amount' => $data['amount'],
]);
}
}Why this doesn’t belong in the store action itself
Nothing in Laravel forces you to use a separate service class. You can call an external API directly from a controller. The problem shows up when one action tries to do all of this at once: validate input, build a payload, authenticate, call the API, handle errors, transform the response, save locally, and send notifications. That kind of method is hard to test and hard to reuse. It ties the integration to a single entry point. Once that logic lives in a service, an Artisan command, a queued job, or a scheduled task can call it too, without duplicating any code.
When keeping it in the store action is the right call
Don’t add a service class just because the app uses Laravel. If a store action only creates a model from already-validated input, a service adds indirection without benefit:
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return redirect()->route('products.show', $product);
}The real question isn’t controller versus service as a rule. It’s whether the operation has enough integration or business behavior to justify its own reusable boundary.
When the integration is part of creating the record
This is where teams most often get it wrong. If creating an order also means creating a customer in Stripe or an ERP, that’s no longer a plain Eloquent create(). It’s a workflow spanning two systems. A service can coordinate that:
public function createOrder(array $data): Order
{
// Create or prepare the local order.
// Create the corresponding external record.
// Store the external identifier locally.
// Return the completed order.
}Laravel’s DB::transaction() protects your database operations, but it can’t make an external API call atomic with your database. If the external call succeeds and your local transaction later rolls back, the external system doesn’t roll back with it. Design around that possibility explicitly. Don’t assume the transaction covers it.
Adding a queued job on top
If the integration is slow or doesn’t need to finish before the response returns, dispatch a job that calls the service:
public function store(StoreOrderRequest $request)
{
$order = Order::create($request->validated());
SyncOrderWithExternalSystem::dispatch($order);
return redirect()->route('orders.show', $order);
}That split gives you three clean layers. The store action starts the workflow. The job decides the work happens asynchronously. The integration service knows how to talk to the external system.
Don’t confuse a service class with a service provider
An OrderIntegrationService holds application behavior. A service provider registers bindings in the container during bootstrapping. You don’t need to put integration logic in a provider just because both have “service” in the name. Use the provider only when the binding itself needs configuration. The integration behavior lives in the service.
Where each responsibility belongs
| Responsibility | Good location |
|---|---|
| Request validation | Form Request |
| Request/response coordination | Controller or store action |
| External API communication | Integration service |
| Complex multi-system workflow | Application/service class |
| Simple persistence | Eloquent model or controller action |
| Slow or unreliable integration | Queued job calling the integration service |
If integration logic in your Laravel app has already sprawled across multiple controllers, that’s a common refactor. Our Laravel development team handles this regularly, including designing the retry and error-handling behavior around it.
Create a service when the operation has real integration or business logic behind it. Reach for one too if the logic needs to be reused elsewhere, or if it’s making your store action hard to read. Skip it when the action is only saving your own data.
Related Answers
Still need help?
Talk to our Laravel experts
If your integrations have outgrown a single controller method, we'll help you draw the right boundary between your application and the systems it talks to.
