AgentSkillsCN

hexagonal

六边形架构(端口与适配器)、洋葱架构,以及它们与清洁架构的关系——通过技术无关的领域逻辑,实现高可测试性。 适用场景:六边形架构、端口与适配器、洋葱架构、驱动/被驱动的适配器、技术无关的领域设计、基于适配器的可测试性。 切勿用于:专门的清洁架构层(使用开发/工匠精神/清洁架构)、微服务边界(使用微服务)、领域模型设计(使用领域驱动设计)。

SKILL.md
--- frontmatter
name: hexagonal
description: |
    Hexagonal Architecture (Ports and Adapters), Onion Architecture, and their relationship to Clean Architecture -- enabling technology-independent domain logic with high testability.
    USE FOR: hexagonal architecture, ports and adapters, onion architecture, driving/driven adapters, technology-independent domain design, adapter-based testability
    DO NOT USE FOR: clean architecture layers specifically (use dev/craftsmanship/clean-architecture), microservice boundaries (use microservices), domain model design (use domain-driven-design)
license: MIT
metadata:
  displayName: "Hexagonal Architecture"
  author: "Tyler-R-Kendrick"
compatibility: claude, copilot, cursor

Hexagonal Architecture (Ports and Adapters)

Overview

Hexagonal Architecture, introduced by Alistair Cockburn in 2005, organizes an application so that the core domain logic is isolated from external concerns (databases, APIs, UIs, message brokers) through ports (interfaces) and adapters (implementations). The goal is to make the application equally drivable by users, programs, automated tests, or batch scripts -- and equally connected to any external system.

The architecture is also known as Ports and Adapters. It shares the same fundamental principle as Onion Architecture (Jeffrey Palermo, 2008) and Clean Architecture (Robert C. Martin): dependencies point inward; the domain depends on nothing external.

The Hexagonal Diagram

code
                        Driving Side (Primary)
                     (things that USE the app)

                    REST    CLI    Tests   Events
                     │       │      │       │
                     ▼       ▼      ▼       ▼
               ┌─────────────────────────────────┐
               │        Primary Adapters          │
               │    (implement driving ports)      │
               │                                   │
         ┌─────┤─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤─────┐
         │     │        Primary Ports              │     │
         │     │    (interfaces the app exposes)   │     │
         │     │                                   │     │
         │     │     ┌───────────────────┐         │     │
         │     │     │                   │         │     │
         │     │     │   Domain Model    │         │     │
         │     │     │   (Pure Business  │         │     │
         │     │     │    Logic)         │         │     │
         │     │     │                   │         │     │
         │     │     └───────────────────┘         │     │
         │     │                                   │     │
         │     │       Secondary Ports             │     │
         │     │   (interfaces the app needs)      │     │
         └─────┤─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┤─────┘
               │      Secondary Adapters           │
               │   (implement driven ports)        │
               └─────────────────────────────────┘
                     │       │       │       │
                     ▼       ▼       ▼       ▼
                  Postgres  Redis  Stripe  Kafka

                        Driven Side (Secondary)
                    (things the app USES)

Core Concepts

Ports (Interfaces)

Ports define the boundaries of the application. They are interfaces -- contracts that describe what the application can do (primary/driving) or what the application needs (secondary/driven).

Port TypeAlso CalledDirectionPurposeExample
Primary PortDriving PortInboundDefines what the app offers to the outside worldIOrderService.PlaceOrder(...)
Secondary PortDriven PortOutboundDefines what the app requires from the outside worldIOrderRepository.Save(...), IPaymentGateway.Charge(...)
code
// Primary port — what the application offers
public interface IOrderService
{
    Task<OrderId> PlaceOrder(PlaceOrderCommand command);
    Task<OrderDto> GetOrder(OrderId id);
    Task CancelOrder(OrderId id);
}

// Secondary port — what the application needs
public interface IOrderRepository
{
    Task<Order?> FindById(OrderId id);
    Task Save(Order order);
}

// Secondary port — what the application needs
public interface IPaymentGateway
{
    Task<PaymentResult> Charge(Money amount, PaymentMethod method);
    Task Refund(PaymentId paymentId, Money amount);
}

Adapters (Implementations)

Adapters are concrete implementations that connect ports to specific technologies. They live outside the domain core.

Adapter TypeAlso CalledImplementsExample
Primary AdapterDriving AdapterUses primary portsREST controller, gRPC handler, CLI command, test harness
Secondary AdapterDriven AdapterImplements secondary portsPostgreSQL repository, Stripe payment adapter, Kafka publisher
code
// Primary adapter — REST controller drives the application
[ApiController]
public class OrdersController : ControllerBase
{
    private readonly IOrderService _orderService; // Primary port

    [HttpPost]
    public async Task<IActionResult> PlaceOrder(PlaceOrderRequest request)
    {
        var command = MapToCommand(request);
        var orderId = await _orderService.PlaceOrder(command);
        return Created($"/orders/{orderId}", new { orderId });
    }
}

// Secondary adapter — PostgreSQL implements the repository port
public class PostgresOrderRepository : IOrderRepository
{
    private readonly DbContext _db;

    public async Task<Order?> FindById(OrderId id)
    {
        return await _db.Orders
            .Include(o => o.Lines)
            .FirstOrDefaultAsync(o => o.Id == id);
    }

    public async Task Save(Order order) { ... }
}

// Secondary adapter — Stripe implements the payment port
public class StripePaymentGateway : IPaymentGateway
{
    private readonly StripeClient _stripe;

    public async Task<PaymentResult> Charge(Money amount, PaymentMethod method)
    {
        var intent = await _stripe.PaymentIntents.CreateAsync(...);
        return MapToResult(intent);
    }
}

Driving vs. Driven Side

AspectDriving (Primary) SideDriven (Secondary) Side
Who initiatesExternal actor drives the applicationApplication drives external systems
Port directionInbound (app receives calls)Outbound (app makes calls)
Adapter roleTranslates external input into domain callsTranslates domain calls into external system interactions
Dependency directionAdapter depends on port (calls it)Adapter implements port (the domain defines the interface)
ExamplesHTTP controller, CLI, test, event consumerDatabase, API client, message publisher, file system

Code Structure Example

code
src/
  OrderService/
    Domain/                          # Pure domain model (no dependencies)
      Order.cs
      OrderLine.cs
      Money.cs
      OrderStatus.cs

    Ports/
      Primary/                       # What the app offers
        IOrderService.cs
        Commands/
          PlaceOrderCommand.cs
          CancelOrderCommand.cs
        Queries/
          GetOrderQuery.cs
      Secondary/                     # What the app needs
        IOrderRepository.cs
        IPaymentGateway.cs
        IInventoryClient.cs
        IEventPublisher.cs

    Application/                     # Use case orchestration
      OrderApplicationService.cs     # Implements IOrderService

    Adapters/
      Primary/                       # Driving adapters
        Rest/
          OrdersController.cs
        Grpc/
          OrderGrpcService.cs
        Cli/
          OrderCliCommand.cs
      Secondary/                     # Driven adapters
        Persistence/
          PostgresOrderRepository.cs
        Payment/
          StripePaymentGateway.cs
        Messaging/
          KafkaEventPublisher.cs

    Composition/                     # Wires everything together (DI)
      ServiceRegistration.cs

The Dependency Rule

The fundamental rule shared by Hexagonal, Onion, and Clean Architecture:

code
Dependencies point inward. Inner layers know nothing about outer layers.

┌─────────────────────────────────────────┐
│  Adapters (outermost)                    │
│  ┌─────────────────────────────────┐    │
│  │  Ports / Application            │    │
│  │  ┌─────────────────────────┐    │    │
│  │  │  Domain Model           │    │    │
│  │  │  (innermost, no deps)   │    │    │
│  │  └─────────────────────────┘    │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

  Outer depends on inner. Never the reverse.
  • The domain has zero external dependencies. No framework imports, no database references, no HTTP concepts.
  • Ports are defined by the domain/application layer using domain language.
  • Adapters depend on ports (and on external libraries), never the reverse.
  • Composition root (startup/DI configuration) wires adapters to ports.

Comparison: Hexagonal vs. Onion vs. Clean Architecture

AspectHexagonal (Cockburn)Onion (Palermo)Clean (Martin)
Core ideaPorts and AdaptersConcentric layersDependency Rule
VisualizationHexagon with portsConcentric circlesConcentric circles
Inner layerDomain ModelDomain ModelEntities
Boundary definitionPorts (interfaces)Layer interfacesUse Case boundaries
Outer layerAdaptersInfrastructureFrameworks & Drivers
Key emphasisSymmetry between driving/drivenLayer disciplineUse cases as central organizing concept
Dependency directionInwardInwardInward

They are the same fundamental idea expressed differently. All three:

  • Isolate the domain from infrastructure.
  • Use interfaces (ports) at boundaries.
  • Enforce the dependency rule: inner layers never reference outer layers.
  • Enable technology swaps without changing business logic.

The practical differences are in emphasis and vocabulary, not in principle.

Onion Architecture Layers (Palermo)

code
┌─────────────────────────────────────────┐
│  Infrastructure & UI (outermost)         │
│  ┌─────────────────────────────────┐    │
│  │  Application Services           │    │
│  │  ┌─────────────────────────┐    │    │
│  │  │  Domain Services        │    │    │
│  │  │  ┌─────────────────┐    │    │    │
│  │  │  │  Domain Model   │    │    │    │
│  │  │  │  (Entities,     │    │    │    │
│  │  │  │   Value Objects)│    │    │    │
│  │  │  └─────────────────┘    │    │    │
│  │  └─────────────────────────┘    │    │
│  └─────────────────────────────────┘    │
└─────────────────────────────────────────┘

Testability: The Primary Benefit

The greatest practical benefit of hexagonal architecture is testability. Because the domain depends only on ports (interfaces), you can test business logic without any infrastructure.

code
// Test the domain using mock adapters — no database, no HTTP, no Stripe
[Test]
public async Task PlaceOrder_WithValidItems_ConfirmsOrder()
{
    // Arrange — mock secondary ports
    var orderRepo = new InMemoryOrderRepository();
    var paymentGateway = new FakePaymentGateway(alwaysSucceeds: true);
    var eventPublisher = new SpyEventPublisher();

    // The application service uses ports, not concrete adapters
    var service = new OrderApplicationService(
        orderRepo, paymentGateway, eventPublisher);

    // Act — drive through primary port
    var orderId = await service.PlaceOrder(new PlaceOrderCommand
    {
        CustomerId = "cust-1",
        Items = new[] { new OrderItem("prod-1", 2, 25.00m) }
    });

    // Assert — verify domain behavior
    var order = await orderRepo.FindById(orderId);
    Assert.Equal(OrderStatus.Confirmed, order.Status);
    Assert.Single(eventPublisher.PublishedEvents
        .OfType<OrderConfirmed>());
}

Testing Strategy by Layer

LayerTest TypeWhat to TestInfrastructure Needed
DomainUnit testsBusiness rules, invariants, calculationsNone (pure logic)
ApplicationUnit tests with mocksUse case orchestration, event publishingMock adapters
Primary AdaptersIntegration testsRequest mapping, serialization, routingHTTP test server
Secondary AdaptersIntegration testsDatabase queries, API calls, serializationReal or containerized infrastructure
CompositionSmoke / E2E testsFull system wiring, happy pathFull infrastructure

Common Mistakes

MistakeProblemFix
Domain imports frameworkDomain coupled to infrastructure; hard to testRemove all framework dependencies from domain layer
Adapter logic in domainBusiness logic leaks into controllers or repositoriesMove logic to domain model or application service
Port too broadInterface with 20 methods; hard to mock, violates ISPSplit into focused interfaces (Interface Segregation Principle)
Skipping ports for "simplicity"Application calls database directly; loses swappability and testabilityAlways define a port even if you only have one adapter
Anemic domain + fat serviceDomain model is just data; all logic in application serviceEnrich the domain model with behavior (see dev/architecture/domain-driven-design)

Best Practices

  • Keep the domain model completely free of infrastructure dependencies. No ORM attributes, no HTTP concepts, no serialization annotations in the domain layer.
  • Define ports using domain language, not technology language. IOrderRepository.Save(Order), not IDatabaseContext.ExecuteCommand(SQL).
  • Use dependency injection to wire adapters to ports at the composition root.
  • Write the majority of tests against ports (mock adapters), not against infrastructure. This gives you fast, reliable tests.
  • Start with one adapter per port. Add additional adapters when you actually need them (e.g., switching databases, adding a CLI interface).
  • Use the hexagonal structure to enable incremental migration: swap one adapter at a time without touching the domain.
  • Combine with DDD (see dev/architecture/domain-driven-design) for rich domain modeling inside the hexagon.
  • The hexagonal shape is a metaphor for symmetry -- there is no inherent "top" or "bottom." Any adapter on any side is equally first-class.