Adapter in Go: Structural Pattern with Clean/Hexagonal Implementation

1/31/2026

Design Pattern
Golang
Structural
Clean Architecture
Hexagonal
Adapter in Go: Structural Pattern with Clean/Hexagonal Implementation thumbnail

Overview

Adapter is a structural design pattern used to translate incompatible third-party interfaces into internal ports. In real backend services, this pattern helps teams keep modules decoupled while still shipping features quickly.

Problem It Solves

Without Adapter, teams often mix orchestration, external integration, and domain rules in one place. That leads to high coupling, difficult testing, and expensive refactoring.

5W + 1H

What

Adapter is a pattern that structures collaboration between components to solve a recurring design problem.

Why

To increase maintainability, isolate change, and enforce clear boundaries between policy and implementation detail.

Who

Backend engineers, platform engineers, and API teams working with integrations and evolving business logic.

When

Use Adapter when your service complexity is growing and one responsibility starts bleeding into unrelated modules.

Where

Apply it in service boundaries such as application use cases, integration adapters, workflow orchestration, and domain policies.

How

Define stable ports first, then implement adapters and wire them through use-case orchestration.

Go Implementation (Step-by-step)

1) Port / interface definitions

go
package ports

import "context"

type AdapterInput struct {
	RequestID string
	Payload   map[string]any
}

type AdapterOutput struct {
	Status string
	Data   map[string]any
}

type AdapterPort interface {
	Execute(ctx context.Context, in AdapterInput) (AdapterOutput, error)
}

2) Adapter / implementation

go
package adapters

import (
	"context"
	"fmt"
	"myapp/internal/ports"
)

type AdapterAdapter struct {
	providerName string
}

func NewAdapterAdapter(providerName string) *AdapterAdapter {
	return &AdapterAdapter{providerName: providerName}
}

func (a *AdapterAdapter) Execute(ctx context.Context, in ports.AdapterInput) (ports.AdapterOutput, error) {
	if in.RequestID == "" {
		return ports.AdapterOutput{}, fmt.Errorf("request id is required")
	}

	return ports.AdapterOutput{
		Status: "ok",
		Data: map[string]any{
			"pattern": "Adapter",
			"provider": a.providerName,
		},
	}, nil
}

3) Use-case / main wiring

go
package main

import (
	"context"
	"fmt"
	"log"
	"myapp/internal/adapters"
	"myapp/internal/ports"
)

func run(ctx context.Context, svc ports.AdapterPort) error {
	out, err := svc.Execute(ctx, ports.AdapterInput{
		RequestID: "REQ-1001",
		Payload: map[string]any{"source": "api"},
	})
	if err != nil {
		return err
	}

	fmt.Println("status:", out.Status, "pattern:", out.Data["pattern"])
	return nil
}

func main() {
	ctx := context.Background()
	service := adapters.NewAdapterAdapter("default-provider")
	if err := run(ctx, service); err != nil {
		log.Fatal(err)
	}
}

Suggested Project Structure

text
cmd/
  api/
    main.go
internal/
  domain/
    adapter-pattern-go/
      model.go
      policy.go
  application/
    adapter-pattern-go/
      usecase.go
  ports/
    adapter-pattern-go_port.go
  adapters/
    inbound/
      http/
        adapter-pattern-go_handler.go
    outbound/
      adapter-pattern-go_adapter.go
  infrastructure/
    config/
      loader.go
    persistence/
      repository.go

Clean/Hexagonal Placement

Boundary Rules

  1. Dependencies point inward: adapters/infrastructure depend on ports/domain, not the reverse.
  2. Domain must stay framework-agnostic (no HTTP, DB driver, or vendor SDK import).
  3. Application layer should know interfaces, not concrete adapters.
  4. Infrastructure may know everything technical, but should not hold business policy.

Real-World Use Case

In production, Adapter is useful to normalize vendor payment response format. You can keep your use case stable while replacing providers or transport implementations with minimal changes.

Benefits & Tradeoffs

Benefits

Tradeoffs

Common Pitfalls

  1. Putting domain rules in adapters.
  2. Leaking vendor-specific payload directly into domain models.
  3. Creating too many abstractions without concrete change pressure.
  4. Skipping contract tests for port behavior.

When NOT to use

Conclusion

Adapter is most effective when paired with Clean/Hexagonal boundaries. The pattern gives structure, while architecture keeps dependencies under control as the codebase evolves.