Skip to content

Latest commit

 

History

History
229 lines (165 loc) · 11.8 KB

File metadata and controls

229 lines (165 loc) · 11.8 KB

Adapter — Structural Design Pattern

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.


What is it?

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 to use it

  • 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.


Adapter vs Facade — side by side

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

Go's approach vs. OOP languages

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.


Structure of this implementation

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)

The target interface

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.

The adaptees

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) error

third_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.


The adapters

RazorpayAdapter

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 ProcessPaymentMakePayment
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)
}

PaytmAdapter

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.

Checkout

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.


Testing

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

Key interview talking points

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.