Skip to content

Expert Answers to Your Magento, Shopify & Shopware Questions

Vendor-neutral, engineer-written explanations. Clear definitions first, then practical steps with real examples — no fluff.

Stagebit builds and maintains stores on Magento, Adobe Commerce, Shopify, Shopware, and Laravel, and this library holds the questions our own engineers get asked most often on each platform.

Should integration logic live inside the store or in a separate Laravel service?

SB
Written by Stagebit Engineering Team
Updated September 2026 4 min read Verified by engineers

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

ResponsibilityGood location
Request validationForm Request
Request/response coordinationController or store action
External API communicationIntegration service
Complex multi-system workflowApplication/service class
Simple persistenceEloquent model or controller action
Slow or unreliable integrationQueued 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.

Was this answer helpful?

Your feedback helps us improve our 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.

Talk to Laravel Experts