This is Adapter, not Facade. Facade simplifies a complex subsystem behind a single interface. Adapter translates an existing interface into a different one that a client expects — the subsystem's complexity is secondary; the incompatibility is the problem being solved.
The Adapter pattern wraps an incompatible third-party type in a new struct that satisfies the interface your application already expects. The third-party type is untouched — the adapter absorbs all the translation work: unit conversion, parameter reordering, currency denomination lookup, retry polling, error mapping, etc.
It sits in the Structural family because it changes how types relate to each other without altering their internal behaviour.
- When you depend on a third-party library whose API does not match the interface your codebase uses.
- When you want to swap payment providers (Razorpay ↔ Paytm ↔ Stripe) without touching the business logic that calls
ProcessPayment. - When the external API uses different units, types, or conventions (e.g. Razorpay expects integer paise; Paytm uses an async initiate-then-poll model).
Avoid it when you control both sides of the interface — just align them directly. Adapter adds an indirection layer that is only justified when the external type cannot be changed.
| Facade | Adapter | |
|---|---|---|
| Problem solved | Too many classes to orchestrate | Interface mismatch between two existing types |
| Changes the interface? | Yes — simplifies/hides it | Yes — translates it to a specific target |
| Wraps how many types? | Often many | Usually one |
| Example here | — | RazorpayAdapter, PaytmAdapter each translate PaymentProcessor calls into provider-specific SDKs |
In Java or C++, Adapter is often a class implementing a target interface while holding a reference to the adaptee. Go's approach is structurally identical but leaner:
| Mechanism | Purpose |
|---|---|
PaymentProcessor interface |
The target contract — what the application expects every payment provider to look like |
RazorpayClient / PaytmClient / StripeClient |
The adaptees — third-party SDKs with their own method signatures and conventions |
RazorpayAdapter / PaytmAdapter |
Hold an instance of the adaptee and translate PaymentProcessor calls into adaptee calls |
paytmClientInterface |
Unexported interface mirroring PaytmClient — enables mock injection without exposing the seam publicly |
NewRazorpayAdapter() / NewPaytmAdapter() |
Constructors — wire the adapter to a concrete adaptee instance |
Go's implicit interface satisfaction means neither adapter needs to declare that it implements PaymentProcessor. If the method set matches, the compiler accepts it.
adapter/
├── main.go # Usage demonstration
├── payment_adapter/
│ ├── payment_processor.go # PaymentProcessor target interface
│ ├── razorpay_adapter.go # Adapter — float rupees → integer paise → MakePayment
│ ├── razorpay_adapter_test.go # Denomination helper + adapter tests
│ ├── paytm_adapter.go # Adapter — initiate-then-poll with retry loop
│ └── paytm_adapter_test.go # Mock client + retry scenario tests
└── third_party_library/
├── razorpay_sdk_client.go # Adaptee — MakePayment(amountInPaise int, currency string)
├── paytm_sdk_client.go # Adaptee — InitiatePayment + GetPaymentStatus
└── stripe_sdk_client.go # Adaptee — ProcessPayment(amount float64, currency string)
payment_adapter/payment_processor.go declares the contract the application codes against:
type PaymentProcessor interface {
ProcessPayment(amount float64, currency string) error
}All business logic depends on this interface. Swapping providers means swapping which adapter is injected — no other code changes.
third_party_library/razorpay_sdk_client.go — Razorpay's SDK uses integer paise and its own method name:
func (r *RazorpayClient) MakePayment(amountInPaise int, currency string) errorthird_party_library/paytm_sdk_client.go — Paytm's SDK is async: initiate returns an ID, then poll for status:
func (p *PaytmClient) InitiatePayment(client, amount, currency, description string, priority int) string
func (p *PaytmClient) GetPaymentStatus(id string) (bool, error)third_party_library/stripe_sdk_client.go — Stripe's SDK matches ProcessPayment in name and signature; a StripeAdapter would be a thin wrapper.
payment_adapter/razorpay_adapter.go bridges the synchronous Razorpay SDK:
| Responsibility | Implementation |
|---|---|
| Currency validation | getLeastDenomination(currency) returns (int, error) — unsupported currencies fail before any SDK call |
| Unit conversion | float64 rupees × denomination → int paise |
| Method translation | ProcessPayment → MakePayment |
func (rpa *RazorpayAdapter) ProcessPayment(amount float64, currency string) error {
leastDenomination, err := getLeastDenomination(currency)
if err != nil {
return err
}
amountInPaise := int(amount * float64(leastDenomination))
return rpa.razorpayClient.MakePayment(amountInPaise, currency)
}payment_adapter/paytm_adapter.go bridges the async Paytm SDK with a retry loop:
| Responsibility | Implementation |
|---|---|
| Async initiation | InitiatePayment starts the transaction and returns a tracking ID |
| Retry polling | Up to 3 GetPaymentStatus calls with exponential back-off (tries × 500ms) |
| Error propagation | Status check errors return immediately without further retries |
| Exhaustion guard | Returns ErrMaxRetriesReached if all 3 polls report pending |
| Testability | sleepFn func(time.Duration) field — production wires in time.Sleep, tests inject a no-op |
func (pa *PaytmAdapter) ProcessPayment(amount float64, currency string) error {
id := pa.paytmClient.InitiatePayment("my-client-app", amount, currency, "...", 1)
for tries := 0; tries < 3; tries++ {
success, err := pa.paytmClient.GetPaymentStatus(id)
if err != nil {
return err
}
if success {
return nil
}
pa.sleepFn(time.Duration(tries) * 500 * time.Millisecond)
}
return ErrMaxRetriesReached
}paytmClientInterface is declared unexported inside the adapter package — PaytmClient satisfies it implicitly, and tests provide a mock without exposing the seam publicly.
func Checkout(processor PaymentProcessor, amount float64, currency string) error {
return processor.ProcessPayment(amount, currency)
}A thin entry point that accepts any PaymentProcessor. This is where dependency injection pays off — the caller decides which provider is active; Checkout is provider-agnostic.
RazorpayAdapter — razorpay_adapter_test.go
| Test | What it verifies |
|---|---|
TestRazorpayAdapterExecution |
Happy path — 123.45 INR flows through without error |
TestRazorpayAdapter_ZeroAmount |
Zero amount converts to 0 paise without error |
TestRazorpayAdapter_LargeAmount |
Large float (9999.99) converts correctly |
TestRazorpayAdapter_UnknownCurrency |
USD is supported — no error |
TestRazorpayAdapter_UnsupportedCurrency |
EUR is not supported — error is returned |
TestGetLeastDenomination_INR |
INR → 100 |
TestGetLeastDenomination_USD |
USD → 100 |
TestGetLeastDenomination_Unsupported |
Unknown currency → non-nil error |
PaytmAdapter — paytm_adapter_test.go
mockPaytmClient drains a pre-loaded queue of (bool, error) pairs on each GetPaymentStatus call — no real network, no real sleeping. newTestPaytmAdapter injects the mock and a no-op sleepFn so retry tests run in microseconds.
| Test | What it verifies |
|---|---|
TestPaytmAdapter_SuccessOnFirstTry |
Status returns true on attempt 1 — exactly 1 poll |
TestPaytmAdapter_SuccessAfterRetries |
Status returns false × 2, then true — exactly 3 polls |
TestPaytmAdapter_MaxRetriesReached |
Status returns false × 3 — returns ErrMaxRetriesReached |
TestPaytmAdapter_StatusCheckError |
Status returns an error — propagates immediately, no further polls |
TestCheckoutWorksWithAllProcessors |
Checkout succeeds with RazorpayAdapter, PaytmAdapter (mock), and a plain mock processor |
Interface compliance is checked at compile time in both test files:
var _ PaymentProcessor = (*RazorpayAdapter)(nil)
var _ PaymentProcessor = (*PaytmAdapter)(nil)Run all tests:
cd structural/adapter && go test ./payment_adapter/... -v
Q: What problem does Adapter solve that Facade doesn't?
Facade reduces complexity — it hides many moving parts behind one simple surface. Adapter solves incompatibility — two types that already exist but whose interfaces don't match. Here, RazorpayClient and PaymentProcessor are both the right level of complexity; they just speak different languages.
Q: Why is the denomination conversion in the adapter rather than the business logic? The denomination format (paise, cents) is a detail of the Razorpay SDK, not a concern of the application. Leaking it into business logic would mean the caller needs to know which provider it's talking to — defeating the purpose of the interface abstraction.
Q: How would you add Stripe support?
Create a StripeAdapter struct that holds a StripeClient and implements ProcessPayment. Since StripeClient.ProcessPayment already uses float64, the adapter may need little more than forwarding the call — but it still provides the seam to add logging, retries, or error mapping without touching either the SDK or the business logic.
Q: Why return an error from getLeastDenomination instead of a default?
A silent default (e.g. multiplier of 1) would process the payment with the wrong amount and produce a real financial error downstream. Failing fast with an explicit error is safer — the caller can handle it before any money moves.
Q: Why does PaytmAdapter have a sleepFn field?
The retry loop uses time.Sleep to back off between polls. In tests, sleeping 0 + 500 + 1000ms per test case makes the suite slow and flaky. Injecting the sleep function lets tests pass a no-op so the retry logic is fully exercised in microseconds. Production always uses time.Sleep via NewPaytmAdapter().
Q: Why is paytmClientInterface unexported?
It is an internal seam for testing — not part of the public API. Exporting it would imply callers should depend on it, which they shouldn't; they depend on PaymentProcessor. Keeping it unexported enforces that boundary while still allowing mock injection inside the same package.
Q: How does Go's implicit interface satisfaction affect Adapter?
Adding a new adapter requires zero changes to the interface or any existing code. As long as the new struct has the right method signatures, the compiler accepts it as a PaymentProcessor. There are no registration steps, no base classes, no implements keywords.