Symfony Bundle · PHP 8.2+ · Packagist

Stop reinventing. Set the standard.

Every API built differently is technical debt. IntegrationEngine gives your team one pattern — an Action, a Mapper, a DTO — repeated across every integration, no exceptions. The structure is not up for debate: it is already defined.

Copied! composer require carlosgude/integration-engine
View on GitHub →

Every integration ends up as an isolated case

If you are on this page, you already know these. They are not new. The question is what you do with the next API you have to integrate.

Days lost per API

No shared pattern means days of discovery and arbitrary decisions every time a new API lands on the backlog.

Onboarding that never scales

A different structure per integration means every new teammate has to start from zero, every time.

Bugs hiding in the gaps

HTTP logic scattered across services, no contract to enforce, nothing to test in isolation.

Zero reuse

Auth, caching, mapping — reinvented from scratch every single time, in every integration.

Impossible to test

HTTP coupled to domain logic. You cannot test business rules without hitting the network.

Nobody knows how it works

Six months later, the developer who built it is gone and the code is unreadable.

You do not have to rewrite anything to start.

IntegrationEngine lives alongside your existing code. Start with the next integration you add to the project. The old ones can keep running as they always have — migrating them becomes a planned technical decision, not an emergency.

One entry point. One contract. Every time.

A single flow for all your external APIs. Every step has a clear, testable owner.

Registry
IntegrationEngine
Action
Auth
HTTP
Mapper
Response DTO

Ship in minutes, not days

make:integration scaffolds Action, Mapper, Response DTO and YAML in one command. You write only business logic.

If you know one, you know them all

Every integration follows the same layout and the same contracts. Instant onboarding for every new teammate.

Token management, zero effort

OAuth, sessions, API keys — fetched, cached, and auto-refreshed on 401. No manual token logic, ever.

Type-safe responses

Every endpoint returns a guaranteed DTO. No guessing at runtime, no silent surprises in production.

Parallel requests, built-in

sendMany() runs N concurrent requests. Individual failures never abort the batch.

Extend at any level

Concrete action, integration facade, or bundle contract — each layer is independently extensible. Swap HTTP client or cache backend with one line in YAML.

The next integration you add can set the standard.

One command generates all the scaffolding. Your team starts writing code that everyone else recognises.

$ php bin/console make:integration MyApi GetEmployee
config/packages/integration_engine.yaml
src/Infrastructure/Integrations/MyApi/MyApiIntegration.php
src/Infrastructure/Integrations/MyApi/MyApi.yaml
src/Infrastructure/Integrations/MyApi/GetEmployee/Request/GetEmployeeAction.php
src/Infrastructure/Integrations/MyApi/GetEmployee/Response/GetEmployeeMapper.php
src/Infrastructure/Integrations/MyApi/GetEmployee/Response/GetEmployeeResponse.php

The same call. Every time.

The engine lives inside your integration facade. From the outside, your domain never knows HTTP exists.

final class MyApiIntegration
{
    public function __construct(
        private IntegrationEngine $engine
    ) {}

    public function listEmployees(): GetEmployeesResponse
    {
        $response = $this->engine->send(
            GetEmployeesAction::getName()
        );
        \assert($response instanceof GetEmployeesResponse);
        return $response;
    }
}
final class MyApiIntegration
{
    public function getEmployee(int $id): GetEmployeeResponse
    {
        $response = $this->engine->send(
            actionName: GetEmployeeAction::getName(),
            context: DefaultActionContext::create(['id' => $id]),
        );
        \assert($response instanceof GetEmployeeResponse);
        return $response;
    }
}
final class MyApiIntegration
{
    public function createEmployee(
        string $name,
        string $correlationId,
    ): CreateEmployeeResponse {
        $response = $this->engine->send(
            actionName: CreateEmployeeAction::getName(),
            body: CreateEmployeeBody::create(['name' => $name]),
            headers: new CorrelationHeaders($correlationId),
        );
        \assert($response instanceof CreateEmployeeResponse);
        return $response;
    }
}
final class MyApiIntegration
{
    public function getUser(int $id): GetUserResponse
    {
        $response = $this->engine->send(
            actionName: GetUserAction::getName(),
            body: GetUserBody::create(['id' => $id]),
        );
        \assert($response instanceof GetUserResponse);
        return $response;
    }
}
final class MyApiIntegration
{
    public function getManyEmployees(array $ids): array
    {
        $requests = [];
        foreach ($ids as $id) {
            $requests[$id] = EngineRequest::create(
                GetEmployeeAction::getName(),
                DefaultActionContext::create(['id' => $id]),
            );
        }
        $results = $this->engine->sendMany($requests);
        if ($results->hasFailures()) {
            throw array_values($results->errors())[0];
        }
        return $results->responses();
    }
}

For the full pattern (facade → service → domain) → README →

Start with the next one.

Your existing integrations do not need to change. The next one can be different.

Already using it? Tell us how it goes →  ·  Prefer to write directly? hi@integrationengine.dev