Bridge in Go: Structural Pattern with Clean/Hexagonal Implementation

1/30/2026

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

Overview

Bridge is a structural design pattern used to separate abstraction from implementation so both can evolve independently. In real backend services, this pattern helps teams keep modules decoupled while still shipping features quickly.

Problem It Solves

Without Bridge, 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

Bridge 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 Bridge 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 BridgeInput struct {
	RequestID string
	Payload   map[string]any
}

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

type BridgePort interface {
	Execute(ctx context.Context, in BridgeInput) (BridgeOutput, error)
}

2) Adapter / implementation

go
package adapters

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

type BridgeAdapter struct {
	providerName string
}

func NewBridgeAdapter(providerName string) *BridgeAdapter {
	return &BridgeAdapter{providerName: providerName}
}

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

	return ports.BridgeOutput{
		Status: "ok",
		Data: map[string]any{
			"pattern": "Bridge",
			"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.BridgePort) error {
	out, err := svc.Execute(ctx, ports.BridgeInput{
		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.NewBridgeAdapter("default-provider")
	if err := run(ctx, service); err != nil {
		log.Fatal(err)
	}
}

Suggested Project Structure

text
cmd/
  api/
    main.go
internal/
  domain/
    bridge-pattern-go/
      model.go
      policy.go
  application/
    bridge-pattern-go/
      usecase.go
  ports/
    bridge-pattern-go_port.go
  adapters/
    inbound/
      http/
        bridge-pattern-go_handler.go
    outbound/
      bridge-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, Bridge is useful to decouple notification workflow from transport channel implementation. 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

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