Zum Hauptinhalt springen
•
YouTube Security VideosMicrosoft Developer: Migrate to Azure SQL. Then Build Something New(20.10.2026 um 23:00 Uhr)
••
YouTube Security VideosMicrosoft Developer: SQL Server 2025: AI-Ready Wherever You Run It(20.10.2026 um 23:00 Uhr)
•••
YouTube Security VideosMicrosoft Developer: Hyperscale for Every Workload(20.10.2026 um 23:00 Uhr)
•
YouTube Security VideosMicrosoft Developer: SQL Meets Fabric: Migrate, Mirror, Build(20.10.2026 um 23:00 Uhr)
••••
YouTube Security VideosMicrosoft Developer: Migrate to Azure SQL. Then Build Something New(20.10.2026 um 23:00 Uhr)
••
YouTube Security VideosMicrosoft Developer: SQL Server 2025: AI-Ready Wherever You Run It(20.10.2026 um 23:00 Uhr)
•••
YouTube Security VideosMicrosoft Developer: Hyperscale for Every Workload(20.10.2026 um 23:00 Uhr)
•
YouTube Security VideosMicrosoft Developer: SQL Meets Fabric: Migrate, Mirror, Build(20.10.2026 um 23:00 Uhr)
•••
Intelligence View
⚡ tsecurity.de Intelligence

The Anti-Corruption Layer: Protecting Your Domain from External APIs

The Anti-Corruption Layer: How to Protect Your Domain from External APIs There comes a point in almost every project when someone says, "We need to connect to…

Beitrag
0
Seite
0
↗ Quelle (dev.to)
Social ReaktionenReagiere als Erste:r — dein Feedback zählt!




The Anti-Corruption Layer: How to Protect Your Domain from External APIs



There comes a point in almost every project when someone says, "We need to connect to API X." Everyone nods. It sounds simple enough. A few hours later, that API's response model has started appearing in controllers, business services, and tests. Before we know it, we've allowed an infrastructure decision to contaminate the core of our application.



This article is about how to prevent that. About a pattern called the Anti-Corruption Layer (ACL): where it comes from, why it matters, and how to apply it in practice with Symfony using IntegrationEngine — a bundle designed from the ground up to make this pattern not optional, but the only way to work.









The Problem: The Outside World Leaks In



Imagine we're integrating with Stripe to manage payments. The endpoint GET /v1/charges/{id} returns something like this:




{
"id": "ch_3abc",
"object": "charge",
"amount": 2000,
"currency": "eur",
"status": "succeeded",
"customer": "cus_xyz",
"payment_method_details": {
"card": {
"brand": "visa",
"last4": "4242"
}
}
}






Without thinking about architecture, we often end up with something like this in a business service:




// OrderService.php
public function confirmOrder(string $chargeId): void
{
$charge = $this->stripeClient->getCharge($chargeId);

if ('succeeded' !== $charge['status']) {
throw new PaymentNotConfirmedException();
}

$this->order->markAsPaid($charge['id'], $charge['amount'] / 100);
}






At first glance, this looks reasonable. But we've just introduced several problems.



First: the domain now speaks Stripe's language. It knows amounts are returned in cents (/ 100). It knows the success status is called succeeded. If Stripe changes its API, that change propagates straight into the heart of our business logic.



Second: if we want to test OrderService, we now need to mock Stripe. Infrastructure has leaked into the domain.



Third: if we switch payment providers tomorrow, we have to rewrite our business service. That's absurd. The domain shouldn't know which payment gateway we're using.









The Concept: Anti-Corruption Layer



The term comes from Domain-Driven Design by Eric Evans (2003). Evans describes it as a translation layer between two models that do not share the same language.



The idea is simple: whenever your system needs to communicate with an external system that has its own model (an API, a third-party service, a legacy system), don't allow that external model to enter your domain directly. Create an intermediate layer that translates the external model into your business language.




[ Domain ]  <->  [ ACL ]  <->  [ External API ]






The domain only sees its own objects. The ACL is the only component that understands the external system's details. If the external system changes, only the ACL changes.



In essence, it's the Dependency Inversion Principle applied to external integrations.









What This Looks Like in Code



Let's go back to Stripe. With an ACL, the business service becomes:




// OrderService.php — pure domain
public function confirmOrder(string $chargeId): void
{
$payment = $this->paymentGateway->getPayment($chargeId);

if (!$payment->isSuccessful()) {
throw new PaymentNotConfirmedException();
}

$this->order->markAsPaid($payment->id, $payment->amount);
}






$this->paymentGateway is a domain interface. Payment is a domain object. The service knows nothing about Stripe.



And in the infrastructure layer, the ACL:




// StripePaymentGateway.php — infrastructure
final class StripePaymentGateway implements PaymentGatewayInterface
{
public function getPayment(string $id): Payment
{
$response = $this->stripeIntegration->getCharge($id);

// Translation happens here
return new Payment(
id: $response->charge->id,
amount: $response->charge->amount / 100, // cents → euros
isSuccessful: 'succeeded' === $response->charge->status,
);
}
}






Payment is a domain object. StripePaymentGateway is pure infrastructure. The translation (/ 100, succeeded) lives here, not in the domain.









The Three Layers of a Proper ACL



In practice, a complete ACL for an external integration has three responsibilities.






1. The Transport Adapter



It knows how to communicate with the API: endpoints, HTTP methods, authentication, retries. It knows nothing about the domain.






2. The Integration DTO



It represents the API response exactly as it arrives. It is an infrastructure object, not a domain object. It reflects the provider's contract, not our business model.




final readonly class ChargeResponse
{
public function __construct(
public readonly string $id,
public readonly int $amount,
public readonly string $status,
public readonly string $currency,
) {}
}









3. The Translator (or Mapper)



It converts the raw API response into a typed DTO. This is where the provider's contract is captured faithfully, preparing the data for the final translation into the domain model.




return GetChargeResponse::create(
charge: Charge::create($response),
);






These three responsibilities are not abstract concepts. They are exactly the files that IntegrationEngine generates and structures for every integration. The bundle is not a convenience wrapper around HttpClient — it is the concrete implementation of this pattern in code.









IntegrationEngine: ACL as Mandatory Architecture



The most important design decision in IntegrationEngine is not technical. It's architectural: it makes it impossible to mix integration logic with domain logic, because its data model is designed so that such a mix simply doesn't make sense.



When you install the bundle and generate an integration, you get the exact structure of an ACL. Not as a suggestion — as the only way to work.




php bin/console make:integration Stripe GetCharge









src/Infrastructure/Integrations/Stripe/
├── StripeIntegration.php
├── Stripe.yaml
└── GetCharge/
├── Request/
│ └── GetChargeAction.php
└── Response/
├── GetChargeMapper.php
└── GetChargeResponse.php






Each file has a single responsibility. None of them know what the others do. And none of them know anything about the domain.






The Action: The Transport Contract






final class GetChargeAction extends AbstractAction
{
public static function getName(): string { return 'GetCharge'; }
public static function hasResponse(): bool { return true; }
public static function mapper(): ?string { return GetChargeMapper::class; }
}









The DTO: Stripe's Model, Faithfully Typed






final readonly class Charge
{
private function __construct(
public readonly string $id,
public readonly int $amount,
public readonly string $status,
public readonly string $currency,
) {}

public static function create(array $data): self
{
return new self(
id: (string) ($data['id'] ?? ''),
amount: (int) ($data['amount'] ?? 0),
status: (string) ($data['status'] ?? ''),
currency: (string) ($data['currency'] ?? ''),
);
}
}









The Mapper: The First Translation






final class GetChargeMapper extends AbstractMapper
{
public static function getAction(): string
{
return GetChargeAction::class;
}

protected static function transform(
AbstractAction $action,
array $response
): ResponseInterface {
return GetChargeResponse::create(
charge: Charge::create($response),
);
}
}









The Facade: The Boundary of the Integration Layer






final class StripeIntegration implements IntegrationName
{
public const string NAME = 'stripe';

private IntegrationEngine $engine;

public function __construct(IntegrationRegistry $registry)
{
$this->engine = $registry->get(self::NAME);
}

public function getCharge(string $id): GetChargeResponse
{
$response = $this->engine->send(
actionName: GetChargeAction::getName(),
context: DefaultActionContext::create(['id' => $id]),
);

\assert($response instanceof GetChargeResponse);

return $response;
}
}












The Second Half: Translating Into the Domain



The bundle takes the integration as far as Stripe's typed DTO. From there, the responsibility is ours.




final class StripePaymentGateway implements PaymentGatewayInterface
{
public function __construct(
private readonly StripeIntegration $stripe,
) {}

public function getPayment(string $chargeId): Payment
{
$response = $this->stripe->getCharge($chargeId);

return new Payment(
id: $response->charge->id,
amount: $response->charge->amount / 100,
isSuccessful: 'succeeded' === $response->charge->status,
currency: strtoupper($response->charge->currency),
);
}
}






The complete stack:




OrderService
└── PaymentGatewayInterface
└── StripePaymentGateway
└── StripeIntegration
└── IntegrationEngine
└── Stripe API






Each layer speaks the language of the layer above it.









Why This Separation Actually Matters






Switching Providers Without Touching the Domain



If we migrate from Stripe to Redsys tomorrow, we simply create another implementation of PaymentGatewayInterface. The domain remains untouched.






Domain Tests Without HTTP






$this->paymentGateway = new class implements PaymentGatewayInterface {
public function getPayment(string $id): Payment
{
return new Payment(
id: $id,
amount: 99.99,
isSuccessful: true
);
}
};






No HTTP. No Stripe. No fixtures.






API Knowledge Is Localized



The knowledge that Stripe uses cents or that succeeded means success exists in one place and one place only.






The Domain Speaks Its Own Language



The domain doesn't need to understand Stripe in order to reason about payments.









The Anti-Pattern to Avoid






// ❌ Wrong — domain depends on infrastructure DTOs
public function confirmOrder(string $chargeId): void
{
$response = $this->stripeIntegration->getCharge($chargeId);

if ('succeeded' !== $response->charge->status) {
throw new PaymentNotConfirmedException();
}

$this->order->markAsPaid(
$response->charge->id,
$response->charge->amount / 100
);
}






This recreates the original problem. The bundle did its job. We skipped ours.




An ACL is not about adding interfaces. It's about translating models.










When You Don't Need a Full ACL



A full ACL may be unnecessary when:




  • The project is small.

  • The API is unlikely to change.

  • The integration is only used in one place.

  • It's a one-off script or migration.



An ACL has a cost: more files, more layers, more indirection. Use it when the domain complexity justifies it.









Conclusion



The Anti-Corruption Layer is not a technology. It's a design decision: protecting your business model from the outside world.



What makes IntegrationEngine interesting is that it turns this decision into the default architecture. You don't need to remember the pattern or convince the team to follow it. The generated structure naturally guides you toward the correct separation of concerns.



The integration layer has its place. DTOs have their place. The facade has its place. Your responsibility is the final translation into the domain: the moment external data stops being external and becomes part of your business language.



A good sign that your ACL is working: if you can replace an external provider without the business team — or your domain tests — noticing, then the layer is doing its job.






The code samples in this article use IntegrationEngine, an MIT-licensed Symfony bundle available via composer require carlosgude/integration-engine.

Intelligence Digest — kostenlos Täglich die wichtigsten Security-News · jederzeit abbestellbar
Community Rating & Social Proof
⭐ Leser-Wertung
–
Ø / 5.0
Noch keine Leser-Bewertungen
War diese Seite hilfreich?
0
Jetzt Bewertung abgeben
🟢 Niedrig (1-3.9) 🟡 Mittel (4.0-6.9) 🔴 Hoch (7.0-8.9) Kritisch (9.0-10.0)

Verwandte Story-Cluster & Quellen (Vektor-KI)

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten The Anti-Corruption Layer: Protecting Your Domain from External APIs

Thematisch verwandte Begriffe: AntiCorruption, Layer, Protecting, Your · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
Nächster Beitrag