Adapter Pattern #

You just found the perfect Go library for your SMS gateway needs — full-featured, fast, and battle-tested. One problem: the library exposes a SendTextMessage(phone, body string) (string, error) method, while your whole system already depends on a Notifier interface with a Notify(recipient, message string) error method. You cannot change the third-party library, and changing the Notifier interface means modifying dozens of files already running in production. The Adapter Pattern exists for exactly this kind of situation — it lets two incompatible interfaces work together without changing either of them, simply by adding a thin translation layer between the two.

What Is the Adapter Pattern? #

The Adapter Pattern is a structural design pattern that converts an object’s interface into another interface the client expects. Like a power plug adapter that lets a type-A device work in a type-C outlet, the Adapter Pattern lets two components that “speak different languages” collaborate without either one having to change.

There are three parties in an Adapter scenario:

  • Client — existing code that expects a specific interface (the Target)
  • Adaptee — an existing object with a different interface you want to use
  • Adapter — a thin layer that implements the Target interface and delegates calls to the Adaptee

The Adapter Pattern answers the question “how do I use X without changing Y?” — a question that comes up constantly when integrating third-party libraries, migrating legacy systems, or connecting microservices with different contracts.

flowchart LR
    subgraph "Without Adapter — Impossible"
        C1[Client\\nneeds Notify] -. "interface\\ndoes not match" .-> A1[SMS Library\\nhas SendTextMessage]
    end

    subgraph "With Adapter — Works"
        C2[Client\\nneeds Notify] -->|Notify| AD[SMSAdapter\\nimplements Notify]
        AD -->|SendTextMessage| A2[SMS Library]
    end

Anatomy of the Problem It Solves #

Before looking at the implementation, it is important to understand three real scenarios where Adapter is the right solution — not just “different interfaces” in the abstract.

Scenario 1: Third-Party Library Integration #

A third-party library cannot be changed, but its interface does not match your system. This is the most common Adapter use case.

// ANTI-PATTERN: client depends directly on a concrete library
import "github.com/twilio/twilio-go"

type NotificationService struct {
    twilioClient *twilio.RestClient // depends on a concrete implementation
}

func (s *NotificationService) SendAlert(phone, message string) error {
    // Calling the Twilio API directly
    params := &openapi.CreateMessageParams{}
    params.SetTo(phone)
    params.SetBody(message)
    _, err := s.twilioClient.Api.CreateMessage(params)
    return err
}
// Problem: swapping Twilio for another provider = changing all this code

// CORRECT: client depends on an interface — the adapter handles library details
type SMSProvider interface {
    SendSMS(phone, message string) error
}

type NotificationService struct {
    sms SMSProvider // interface, not a concrete type
}

func (s *NotificationService) SendAlert(phone, message string) error {
    return s.sms.SendSMS(phone, message)
}
// Swapping Twilio for Vonage = swapping the adapter only, NotificationService stays untouched

Scenario 2: Legacy System Migration #

When migrating a system step by step, old code and new code must coexist. An Adapter lets new code use a modern interface while the implementation still delegates to the old system.

Scenario 3: Unifying Multiple Providers #

When several vendors with different APIs must appear uniform to business code — payment gateways like Midtrans, Xendit, and Stripe, for example — adapters unify all three behind one consistent interface.


Structure and Components #

The Adapter Pattern involves four components with clear, non-overlapping roles.

flowchart TD
    subgraph Client Layer
        C[OrderService\\nclient code]
    end

    subgraph Target Interface
        T[PaymentProcessor\\ninterface\\n+Pay amount error]
    end

    subgraph Adapter Layer
        MA[MidtransAdapter\\n+Pay amount error]
        XA[XenditAdapter\\n+Pay amount error]
    end

    subgraph Adaptee Layer
        M[Midtrans SDK\\n+ChargeWithCreditCard]
        X[Xendit SDK\\n+CreateInvoice]
    end

    C -->|only knows| T
    T --> MA
    T --> XA
    MA -->|translate| M
    XA -->|translate| X
ComponentRoleWhat Changes When Swapping Providers
Target (interface)The contract the client expectsNever changes
ClientUses the Target interfaceNever changes
AdapterTranslates Target to AdapteeSwap or add a new Adapter
AdapteeLibrary/system with a different interfaceUntouched

Full Implementation: Multi Payment Gateway #

Let’s build a payment system that works with Midtrans, Xendit, and Stripe — three very different APIs — behind one uniform interface.

The Target Interface #

This interface is the “language” understood by all business code. Every adapter must implement it.

package payment

import "time"

// TransactionResult stores the result of a successful transaction.
type TransactionResult struct {
    TransactionID string
    Amount        int
    Currency      string
    Method        string
    ProcessedAt   time.Time
    RedirectURL   string // for payments that need a redirect (ewallet, VA)
}

// PaymentProcessor is the Target interface — the contract for all payment providers.
// Business code only knows this interface; no concrete provider is ever mentioned.
type PaymentProcessor interface {
    // Pay processes a payment and returns the transaction result.
    Pay(amount int, currency, description string) (*TransactionResult, error)

    // Refund cancels a transaction and returns the funds.
    Refund(transactionID string, amount int) error

    // CheckStatus checks a transaction's status by its ID.
    CheckStatus(transactionID string) (string, error)

    // ProviderName returns the provider name for logging and auditing.
    ProviderName() string
}

Adaptee 1: The Midtrans SDK #

Midtrans uses the concept of “charge” with a request structure specific to each payment method.

package midtrans

// This represents the Midtrans Go SDK that you cannot modify.
// Its interface is completely different from PaymentProcessor.

type ChargeRequest struct {
    PaymentType   string
    TransactionID string
    GrossAmount   int64
    Description   string
    CustomerName  string
    CustomerEmail string
}

type ChargeResponse struct {
    TransactionID     string
    OrderID           string
    GrossAmount       string
    PaymentType       string
    TransactionStatus string
    FraudStatus       string
    RedirectURL       string
}

type Client struct {
    serverKey string
    baseURL   string
}

func NewClient(serverKey, baseURL string) *Client {
    return &Client{serverKey: serverKey, baseURL: baseURL}
}

func (c *Client) Charge(req ChargeRequest) (*ChargeResponse, error) {
    // Midtrans API call implementation
    return &ChargeResponse{
        TransactionID:     "midtrans-" + req.TransactionID,
        TransactionStatus: "capture",
    }, nil
}

func (c *Client) CancelTransaction(transactionID string) error {
    // Cancel implementation via the Midtrans API
    return nil
}

func (c *Client) GetTransactionStatus(transactionID string) (*ChargeResponse, error) {
    return &ChargeResponse{TransactionStatus: "capture"}, nil
}

Adaptee 2: The Xendit SDK #

Xendit uses an “invoice” concept that is far removed from Midtrans.

package xendit

// Xendit Go SDK — yet another different interface.

type InvoiceRequest struct {
    ExternalID  string
    Amount      float64
    Description string
    PayerEmail  string
    Currency    string
    CallbackURL string
    SuccessURL  string
}

type InvoiceResponse struct {
    ID         string
    ExternalID string
    Amount     float64
    Status     string
    InvoiceURL string
    Created    string
    Expiry     string
}

type Client struct {
    apiKey string
}

func NewClient(apiKey string) *Client {
    return &Client{apiKey: apiKey}
}

func (c *Client) CreateInvoice(req InvoiceRequest) (*InvoiceResponse, error) {
    return &InvoiceResponse{
        ID:         "xendit-inv-123",
        Status:     "PENDING",
        InvoiceURL: "https://checkout.xendit.co/inv/xendit-inv-123",
    }, nil
}

func (c *Client) ExpireInvoice(invoiceID string) error {
    return nil
}

func (c *Client) GetInvoice(invoiceID string) (*InvoiceResponse, error) {
    return &InvoiceResponse{Status: "PAID"}, nil
}

Adapter 1: MidtransAdapter #

package adapter

import (
    "fmt"
    "time"

    "myapp/midtrans"
    "myapp/payment"
)

// MidtransAdapter implements payment.PaymentProcessor
// by delegating all calls to the Midtrans SDK.
type MidtransAdapter struct {
    client      *midtrans.Client
    paymentType string // "credit_card", "gopay", "bank_transfer", etc.
}

func NewMidtransAdapter(client *midtrans.Client, paymentType string) payment.PaymentProcessor {
    return &MidtransAdapter{
        client:      client,
        paymentType: paymentType,
    }
}

// Pay converts a PaymentProcessor.Pay() call into a Midtrans Charge().
// This is the heart of the adapter — translation between two different "languages".
func (a *MidtransAdapter) Pay(amount int, currency, description string) (*payment.TransactionResult, error) {
    req := midtrans.ChargeRequest{
        PaymentType:   a.paymentType,
        TransactionID: fmt.Sprintf("order-%d", time.Now().UnixNano()),
        GrossAmount:   int64(amount),
        Description:   description,
    }

    resp, err := a.client.Charge(req)
    if err != nil {
        return nil, fmt.Errorf("midtrans charge failed: %w", err)
    }

    // Translate the Midtrans response into a standard TransactionResult
    return &payment.TransactionResult{
        TransactionID: resp.TransactionID,
        Amount:        amount,
        Currency:      currency,
        Method:        "midtrans/" + a.paymentType,
        ProcessedAt:   time.Now(),
        RedirectURL:   resp.RedirectURL,
    }, nil
}

func (a *MidtransAdapter) Refund(transactionID string, amount int) error {
    // Midtrans uses "cancel" for refunds
    return a.client.CancelTransaction(transactionID)
}

func (a *MidtransAdapter) CheckStatus(transactionID string) (string, error) {
    resp, err := a.client.GetTransactionStatus(transactionID)
    if err != nil {
        return "", fmt.Errorf("midtrans status check failed: %w", err)
    }
    // Translate the Midtrans status into a standard status
    return normalizeMidtransStatus(resp.TransactionStatus), nil
}

func (a *MidtransAdapter) ProviderName() string {
    return "Midtrans"
}

// normalizeMidtransStatus converts Midtrans-specific statuses into standard statuses.
func normalizeMidtransStatus(midtransStatus string) string {
    switch midtransStatus {
    case "capture", "settlement":
        return "success"
    case "pending":
        return "pending"
    case "cancel", "expire":
        return "failed"
    default:
        return "unknown"
    }
}

Adapter 2: XenditAdapter #

package adapter

import (
    "fmt"
    "time"

    "myapp/xendit"
    "myapp/payment"
)

// XenditAdapter implements payment.PaymentProcessor
// by delegating all calls to the Xendit SDK.
type XenditAdapter struct {
    client      *xendit.Client
    callbackURL string
    successURL  string
}

func NewXenditAdapter(client *xendit.Client, callbackURL, successURL string) payment.PaymentProcessor {
    return &XenditAdapter{
        client:      client,
        callbackURL: callbackURL,
        successURL:  successURL,
    }
}

// Pay converts a PaymentProcessor.Pay() call into a Xendit CreateInvoice().
// Xendit is invoice-based — a different concept from Midtrans's charge-based model.
func (a *XenditAdapter) Pay(amount int, currency, description string) (*payment.TransactionResult, error) {
    req := xendit.InvoiceRequest{
        ExternalID:  fmt.Sprintf("inv-%d", time.Now().UnixNano()),
        Amount:      float64(amount),
        Description: description,
        Currency:    currency,
        CallbackURL: a.callbackURL,
        SuccessURL:  a.successURL,
    }

    resp, err := a.client.CreateInvoice(req)
    if err != nil {
        return nil, fmt.Errorf("xendit create invoice failed: %w", err)
    }

    return &payment.TransactionResult{
        TransactionID: resp.ID,
        Amount:        amount,
        Currency:      currency,
        Method:        "xendit/invoice",
        ProcessedAt:   time.Now(),
        RedirectURL:   resp.InvoiceURL, // Xendit always requires a redirect to the invoice page
    }, nil
}

func (a *XenditAdapter) Refund(transactionID string, amount int) error {
    // Xendit uses "expire" to cancel an invoice
    return a.client.ExpireInvoice(transactionID)
}

func (a *XenditAdapter) CheckStatus(transactionID string) (string, error) {
    resp, err := a.client.GetInvoice(transactionID)
    if err != nil {
        return "", fmt.Errorf("xendit status check failed: %w", err)
    }
    return normalizeXenditStatus(resp.Status), nil
}

func (a *XenditAdapter) ProviderName() string {
    return "Xendit"
}

func normalizeXenditStatus(xenditStatus string) string {
    switch xenditStatus {
    case "PAID", "SETTLED":
        return "success"
    case "PENDING":
        return "pending"
    case "EXPIRED":
        return "failed"
    default:
        return "unknown"
    }
}

Client Code: Clean, Unaware of Providers #

package service

import (
    "fmt"
    "myapp/payment"
)

// OrderService is the client — it only knows the PaymentProcessor interface.
// Not a single "Midtrans" or "Xendit" name appears here.
type OrderService struct {
    processor payment.PaymentProcessor
}

func NewOrderService(processor payment.PaymentProcessor) *OrderService {
    return &OrderService{processor: processor}
}

func (s *OrderService) Checkout(orderID string, amount int) (*payment.TransactionResult, error) {
    result, err := s.processor.Pay(amount, "IDR", fmt.Sprintf("Payment for order %s", orderID))
    if err != nil {
        return nil, fmt.Errorf("checkout failed for order %s: %w", orderID, err)
    }

    fmt.Printf("Payment processed via %s: %s\n", s.processor.ProviderName(), result.TransactionID)
    return result, nil
}

func (s *OrderService) ProcessRefund(transactionID string, amount int) error {
    if err := s.processor.Refund(transactionID, amount); err != nil {
        return fmt.Errorf("refund failed for transaction %s: %w", transactionID, err)
    }
    fmt.Printf("Refund processed via %s\n", s.processor.ProviderName())
    return nil
}

func (s *OrderService) CheckPaymentStatus(transactionID string) (string, error) {
    return s.processor.CheckStatus(transactionID)
}

Wiring in main.go — the only place concrete providers are mentioned:

func main() {
    // Pick the provider based on configuration — the only place providers are named
    var processor payment.PaymentProcessor

    switch cfg.PaymentProvider {
    case "midtrans":
        midtransClient := midtrans.NewClient(cfg.MidtransServerKey, cfg.MidtransBaseURL)
        processor = adapter.NewMidtransAdapter(midtransClient, "credit_card")

    case "xendit":
        xenditClient := xendit.NewClient(cfg.XenditAPIKey)
        processor = adapter.NewXenditAdapter(xenditClient, cfg.CallbackURL, cfg.SuccessURL)

    default:
        log.Fatalf("unknown payment provider: %s", cfg.PaymentProvider)
    }

    // OrderService does not know which provider is active
    orderSvc := service.NewOrderService(processor)
    result, err := orderSvc.Checkout("ORD-001", 150000)
    // ...
}

Adapter for Incremental System Migration #

One of the most important Adapter Pattern use cases is enabling migration from a legacy system to a new one step by step, without a risky big-bang rewrite.

sequenceDiagram
    participant C as Client (new code)
    participant A as LegacyAdapter
    participant L as Legacy System

    Note over C,L: Phase 1: Adapter bridges new code to the legacy system
    C->>A: Notify(recipient, message)
    A->>L: SendEmail(to, subject, body)
    L-->>A: legacy response
    A-->>C: standard error

    Note over C,L: Phase 2: When the new system is ready, swap the adapter — client unchanged
    C->>A: Notify(recipient, message)
    Note over A: new adapter — talks to the new system directly
    A-->>C: standard error
// LegacyEmailSystem is the old system that cannot be changed
type LegacyEmailSystem struct {
    smtpServer string
    smtpPort   int
}

func (l *LegacyEmailSystem) SendEmail(to, subject, body, fromName, fromEmail string) int {
    // Returns an integer status code — not an error
    fmt.Printf("Sending via legacy SMTP to %s\n", to)
    return 200
}

func (l *LegacyEmailSystem) GetQueueSize() int {
    return 0
}

// Target interface — the new system's contract
type Notifier interface {
    Notify(recipient, message string) error
}

// LegacyEmailAdapter wraps the legacy system to make it compatible with Notifier
type LegacyEmailAdapter struct {
    legacy *LegacyEmailSystem
}

func NewLegacyEmailAdapter(legacy *LegacyEmailSystem) Notifier {
    return &LegacyEmailAdapter{legacy: legacy}
}

func (a *LegacyEmailAdapter) Notify(recipient, message string) error {
    // Translation: Notifier.Notify() → LegacyEmailSystem.SendEmail()
    // Including the return-type translation: int → error
    statusCode := a.legacy.SendEmail(
        recipient,
        "Notification",  // default subject — the legacy system always needs one
        message,
        "System",        // default fromName
        "[email protected]",
    )

    if statusCode != 200 {
        return fmt.Errorf("legacy email failed with status code: %d", statusCode)
    }
    return nil
}

// When the new system is ready, create a new adapter pointing at the new implementation.
// No client code changes.
type ModernEmailAdapter struct {
    modernClient *modernmail.Client
}

func NewModernEmailAdapter(client *modernmail.Client) Notifier {
    return &ModernEmailAdapter{modernClient: client}
}

func (a *ModernEmailAdapter) Notify(recipient, message string) error {
    return a.modernClient.Send(recipient, message)
}

Testing an Adapter #

Testing an Adapter focuses on two things: ensuring the parameter translation is correct, and ensuring the response/error translation is correct. Because the Adapter delegates to the Adaptee, you can mock the Adaptee for isolated tests.

// MockMidtransClient for testing MidtransAdapter without a connection to Midtrans
type MockMidtransClient struct {
    ChargeFunc             func(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error)
    CancelTransactionFunc  func(transactionID string) error
    GetTransactionStatusFn func(transactionID string) (*midtrans.ChargeResponse, error)
}

func (m *MockMidtransClient) Charge(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error) {
    return m.ChargeFunc(req)
}

func (m *MockMidtransClient) CancelTransaction(id string) error {
    return m.CancelTransactionFunc(id)
}

func (m *MockMidtransClient) GetTransactionStatus(id string) (*midtrans.ChargeResponse, error) {
    return m.GetTransactionStatusFn(id)
}

func TestMidtransAdapter_Pay_Success(t *testing.T) {
    mockClient := &MockMidtransClient{
        ChargeFunc: func(req midtrans.ChargeRequest) (*midtrans.ChargeResponse, error) {
            // Verify the parameters sent to Midtrans
            if req.GrossAmount != 150000 {
                t.Errorf("expected GrossAmount 150000, got %d", req.GrossAmount)
            }
            return &midtrans.ChargeResponse{
                TransactionID:     "mid-txn-123",
                TransactionStatus: "capture",
            }, nil
        },
    }

    adapter := adapter.NewMidtransAdapter(mockClient, "credit_card")
    result, err := adapter.Pay(150000, "IDR", "Test payment")

    if err != nil {
        t.Fatalf("expected no error, got: %v", err)
    }
    if result.TransactionID != "mid-txn-123" {
        t.Errorf("expected TransactionID 'mid-txn-123', got %q", result.TransactionID)
    }
    if result.Method != "midtrans/credit_card" {
        t.Errorf("unexpected method: %s", result.Method)
    }
}

func TestMidtransAdapter_CheckStatus_TranslatesStatus(t *testing.T) {
    tests := []struct {
        midtransStatus string
        expectedStatus string
    }{
        {"capture", "success"},
        {"settlement", "success"},
        {"pending", "pending"},
        {"cancel", "failed"},
        {"expire", "failed"},
        {"unknown_status", "unknown"},
    }

    for _, tt := range tests {
        t.Run(tt.midtransStatus, func(t *testing.T) {
            mockClient := &MockMidtransClient{
                GetTransactionStatusFn: func(id string) (*midtrans.ChargeResponse, error) {
                    return &midtrans.ChargeResponse{TransactionStatus: tt.midtransStatus}, nil
                },
            }

            adpt := adapter.NewMidtransAdapter(mockClient, "credit_card")
            status, err := adpt.CheckStatus("txn-123")

            if err != nil {
                t.Fatalf("unexpected error: %v", err)
            }
            if status != tt.expectedStatus {
                t.Errorf("for midtrans status %q: expected %q, got %q",
                    tt.midtransStatus, tt.expectedStatus, status)
            }
        })
    }
}

Where to Place an Adapter #

An Adapter is infrastructure code, not business code. Placing it correctly keeps the domain clean.

lib/
  ├── domain/
  │   ├── payment/
  │   │   └── processor.go       ← Target interface lives here (domain/port)
  │   └── notification/
  │       └── notifier.go        ← Target interface lives here
  │
  ├── infrastructure/            ← Adapters always live here
  │   ├── payment/
  │   │   ├── midtrans_adapter.go
  │   │   ├── xendit_adapter.go
  │   │   └── stripe_adapter.go
  │   └── notification/
  │       ├── twilio_adapter.go
  │       └── sendgrid_adapter.go
  │
  └── main.go                    ← Adapter wiring + injection

Adapter and Hexagonal Architecture

The Adapter Pattern is the foundation of Hexagonal Architecture (also known as Ports & Adapters). The interfaces in the domain layer are “ports” — contracts that define what the domain needs. The adapters in the infrastructure layer are “adapters” — concrete implementations that connect ports to the outside world. Understanding the Adapter Pattern means already understanding half of Hexagonal Architecture.


Adapter vs Other Structural Patterns #

Adapter is often confused with Facade and Decorator because all three “wrap” something. The difference lies in the purpose of the wrapping.

PatternPurposeOutput Interface
AdapterConverts an incompatible interfaceDifferent from what is wrapped
FacadeSimplifies a complex subsystemDifferent from what is wrapped, but simpler
DecoratorAdds behavior without changing the interfaceSame as what is wrapped
ProxyControls access to an objectSame as what is wrapped
// Adapter: the OUTPUT interface differs from the Adaptee's interface
type LegacyGateway interface { MakePayment(total int) error }
type PaymentProcessor interface { Pay(amount int, currency string) error }
// LegacyGateway → PaymentProcessor: two different interfaces

// Decorator: the OUTPUT interface is the same as what is wrapped
type PaymentProcessor interface { Pay(amount int, currency string) error }
// LoggingDecorator implements PaymentProcessor, wraps PaymentProcessor
// Same interface, added behavior

// Facade: hides complexity, does not convert
type PaymentFacade struct { /* hide auth, retry, logging */ }
func (f *PaymentFacade) SimpleCheckout(amount int) error { /* simplify */ }

When to Use and When Not to #

USE Adapter if:
  ✓ You want to use a third-party library with a different interface
  ✓ You are migrating from a legacy system to a new one step by step
  ✓ You need to unify several different providers behind one interface
  ✓ You want to isolate business code from external API changes
  ✓ You need to make a legacy component testable with mocks

AVOID Adapter if:
  ✗ You control both sides of the code — refactoring directly is better
  ✗ The interface that needs "adapting" is too wide (many methods) — a Facade may be needed
  ✗ The Adapter contains business logic — that is not an adapter's job
  ✗ There is only one provider and no plan to swap it — over-engineering

Adapter Review Checklist #

DESIGN:
  □ The Target interface is defined in the domain/business layer, not in infrastructure
  □ The Adapter only contains translation — no business logic
  □ The client depends on an interface, not a concrete adapter

IMPLEMENTATION:
  □ The Adapter implements every method of the Target interface
  □ Errors from the Adaptee are wrapped with informative context (fmt.Errorf + %w)
  □ Return types are converted to standard domain types (not third-party SDK types)
  □ Vendor-specific statuses/codes are translated into standard domain statuses

PLACEMENT:
  □ The Adapter lives in the infrastructure layer, not the domain layer
  □ Imports of third-party libraries exist only in adapters — not in the domain
  □ Adapter wiring (injection) happens in main or the composition root

TESTING:
  □ The Adaptee is mocked for isolated tests
  □ Parameter translation is verified (the values sent to the Adaptee are correct)
  □ Response and error translation are verified
  □ Status mapping is tested for every possible vendor value

Summary #

  • Adapter bridges two incompatible interfaces — without changing either of them; it only adds a thin translation layer in between.
  • Three key components: the Target interface (what the client expects), the Adaptee (what already exists), and the Adapter (the translator between them).
  • In Go it is always an Object Adapter — composition, not inheritance; the Adapter holds a reference to the Adaptee and delegates calls to it.
  • An Adapter may only contain translation — method mapping, parameter conversion, status normalization; if business logic creeps in, that is a sign of a bad design.
  • Place it in the infrastructure layer — the Target interface in the domain layer, the Adapter in the infrastructure layer; this keeps the domain clean of dependencies on external libraries.
  • Foundation of Hexagonal Architecture — domain interfaces are “ports”, infrastructure adapters are “adapters”; understanding the Adapter Pattern means understanding the core of Ports & Adapters architecture.
  • Distinguish it from Decorator and Facade: Adapter converts a different interface; Decorator adds behavior with the same interface; Facade simplifies a complex subsystem.
  • Use it for incremental migration — new code can run on top of a legacy system through an adapter, buying time for migration without a big-bang rewrite.

← Previous: Prototype   Next: Bridge →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact