When integrating multiple third-party APIs that serve similar purposes (payment gateways, shipping providers, SMS services), you’ll quickly notice they all handle errors differently. One throws CardException with a specific message, another returns error codes like INSUFFICIENT_FUNDS—same problem, completely different implementations.
Instead of scattering vendor-specific error handling throughout your Laravel codebase, wrap their exceptions in custom domain exceptions.
The Problem: Inconsistent Error Handling
// Payment provider 1 (Stripe)
try {
$payment = $stripeClient->charge($amount);
} catch (\Stripe\Exception\CardException $e) {
if (str_contains($e->getMessage(), 'insufficient funds')) {
// Handle insufficient funds
}
}
// Payment provider 2 (PayPal)
try {
$payment = $paypalClient->charge($amount);
} catch (\PayPal\Exception\PayPalException $e) {
if ($e->getCode() === 'INSUFFICIENT_FUNDS') {
// Handle insufficient funds... differently
}
}
// Every integration handles the same error differently
This approach doesn’t scale. Every time you add a new provider, you need to update error handling logic throughout your application. Testing becomes harder because you’re coupled to vendor exception types.
The Solution: Domain Exceptions with Adapters
Create custom exceptions that represent business scenarios (insufficient funds, declined payment, timeout) rather than vendor-specific errors:
// Define domain exceptions
namespace App\Exceptions\Payment;
class InsufficientFundsException extends PaymentException
{
public function __construct(
public readonly string $provider,
public readonly string $transactionId,
string $message = 'Insufficient funds to complete transaction'
) {
parent::__construct($message);
}
}
class PaymentDeclinedException extends PaymentException { /* ... */ }
class PaymentTimeoutException extends PaymentException { /* ... */ }
Now wrap vendor exceptions in adapter classes that implement a common interface:
class StripeAdapter implements PaymentGateway
{
public function charge(Order $order): Transaction
{
try {
return $this->client->createCharge([
'amount' => $order->total,
'currency' => $order->currency,
]);
} catch (\Stripe\Exception\CardException $e) {
// Map Stripe errors to domain exceptions
if (str_contains($e->getMessage(), 'insufficient funds')) {
throw new InsufficientFundsException(
provider: 'stripe',
transactionId: $e->getRequestId()
);
}
throw PaymentException::fromVendorException('stripe', $e);
}
}
}
class PayPalAdapter implements PaymentGateway
{
public function charge(Order $order): Transaction
{
try {
return $this->client->createPayment([/* ... */]);
} catch (\PayPal\Exception\PayPalException $e) {
// Map PayPal errors to same domain exceptions
if ($e->getCode() === 'INSUFFICIENT_FUNDS') {
throw new InsufficientFundsException(
provider: 'paypal',
transactionId: $e->getCorrelationId()
);
}
throw PaymentException::fromVendorException('paypal', $e);
}
}
}
Now Handle All Providers Consistently
class CheckoutService
{
public function processPayment(Order $order, PaymentGateway $gateway)
{
try {
$transaction = $gateway->charge($order);
$order->markAsPaid($transaction);
} catch (InsufficientFundsException $e) {
// Same handler for ALL providers
$this->notifyUser($order->user, 'payment_failed_insufficient_funds');
$this->logPaymentFailure($e->provider, $e->transactionId);
} catch (PaymentDeclinedException $e) {
// ...
}
}
}
Why This Matters
- Consistent interface: Your application logic doesn’t care which payment provider is being used
- Easy to add providers: New integrations just need to implement the
PaymentGatewayinterface and map their errors - Easy to switch providers: Change providers without touching your application logic
- Better testing: Mock domain exceptions instead of vendor-specific ones
- Provider-specific data: Custom exceptions can carry provider IDs, transaction IDs, and other metadata while presenting a unified interface
This pattern works for any multi-provider integration: shipping APIs, SMS services, email providers—anything where you need to abstract vendor differences behind a common interface.
Leave a Reply