Payment Gateways as Anti-Corruption Layers: Applying Hexagonal Architecture in Real-World PHP

Payment Gateways as Anti-Corruption Layers: Applying Hexagonal Architecture in Real-World PHP

# php# opensource# webdev# architecture
Payment Gateways as Anti-Corruption Layers: Applying Hexagonal Architecture in Real-World PHPMohamed Jadla

Payment Gateways as Anti-Corruption Layers Most PHP applications integrate payments directly using...

Payment Gateways as Anti-Corruption Layers

Most PHP applications integrate payments directly using provider SDKs.

Example:\Stripe\Stripe::setApiKey($key);
\Stripe\PaymentIntent::create([...]);
This works.

Until it doesn’t.

The real architectural question isn’t how to charge a card.

It’s:

Should your domain know that Stripe exists?
The Hidden Coupling Problem

When business logic calls Stripe SDK directly, your domain layer now depends on:

Stripe’s request models

Stripe’s response objects

Stripe’s exception types

Stripe’s lifecycle semantics

This is infrastructure bleeding into your core domain.

And that’s where architecture begins to matter.

Hexagonal Architecture Perspective

In Hexagonal Architecture (Ports & Adapters):

The Domain sits at the center.

Infrastructure lives outside.

Communication happens through defined ports.

So the real question becomes:

Should Stripe be inside the hexagon?

The answer is no.

Stripe is infrastructure.

Introducing a Payment Port

Instead of calling Stripe directly, the domain defines a port:

interface PaymentPort {
public function charge(Money $amount): PaymentResult;
}

The application layer depends only on this interface.

No Stripe.
No PayPal.
No SDK types.

Only domain language.

Stripe as an Adapter Boundary

Stripe now becomes an Adapter:

Domain → PaymentPort → StripeAdapter → Stripe SDK

The adapter:

Translates domain objects into Stripe requests

Maps Stripe responses into domain DTOs

Converts SDK exceptions into domain exceptions

This is the Adapter Boundary.

Payment Layer as an Anti-Corruption Layer

In Domain-Driven Design, an Anti-Corruption Layer (ACL) protects your domain from external models.

Stripe has its own language:

PaymentIntent

SetupIntent

CheckoutSession

Your domain might speak:

Charge

Subscription

Invoice

Without an ACL, your domain starts speaking Stripe’s language.

That’s architectural erosion.

An abstraction layer acts as an Anti-Corruption Layer.

It prevents provider vocabulary from leaking inward.

Domain Isolation in Practice

With proper isolation:

✔ You can switch providers without touching business logic
✔ You can mock the PaymentPort in tests
✔ You can simulate failures cleanly
✔ You maintain language consistency in the domain

Your domain remains pure.

But Let’s Be Honest

This adds:

More files

More interfaces

More mapping logic

More maintenance cost

If you only ever use one provider,
this might be premature abstraction.

Architecture is about trade-offs, not dogma.

The Real Decision Framework

Ask yourself:

Is the payment provider core to the business?

Will we expand internationally?

Do we expect regulatory constraints?

Are we building a long-lived SaaS?

If yes → abstraction is insurance.

If not → direct SDK integration may be pragmatic.

A Subtle Insight

Payment gateways are not just APIs.

They are external bounded contexts.

Treating them as simple SDK calls ignores that reality.

The moment billing logic becomes business-critical,
you are no longer integrating an API —
you are integrating another domain.

That’s when Anti-Corruption Layers matter.

Final Thought

Architecture isn’t about adding layers.

It’s about controlling dependency direction.

The question isn’t:

“How do we integrate Stripe?”

The question is:

“How do we prevent Stripe from owning our domain?”

Curious how other senior engineers approach this.

Do you abstract payment providers from day one?

Or only after the second integration becomes real?

👀 githup https://github.com/jadlamed007007007/multi-payment
👀 packagist https://packagist.org/packages/mohamedjadla/multi-payment