← Назад к вопросам

Что такое гексагональная архитектура?

1.8 Middle🔥 181 комментариев
#SOLID и паттерны проектирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Что такое гексагональная архитектура?

Определение

Гексагональная архитектура (Hexagonal Architecture), также известная как Ports & Adapters, — это архитектурный паттерн, предложенный Alistair Cockburn, который изолирует бизнес-логику от технических деталей.

Основа: изолировать бизнес-логику (домен) от внешних сервисов через Ports (интерфейсы) и Adapters (реализации).

Структура гексагональной архитектуры

┌─────────────────────────────────────────────────┐
│              Внешний мир (2/3 части)            │
│  REST API  │ SOAP  │ WebSocket  │ Database     │
└──────────┬──────────────────────────────────────┘
           │ Adapter
┌──────────▼──────────────────────────────────────┐
│                PORT (Interface)                 │
│  UserRepository, PaymentProcessor, EventBus   │
└──────────┬──────────────────────────────────────┘
           │ Implements
┌──────────▼──────────────────────────────────────┐
│           HEXAGON (Core/Domain)                │
│         User Entities & Use Cases               │
│    - CreateUserUseCase                         │
│    - UpdateUserUseCase                         │
│    - GetUserUseCase                            │
└──────────┬──────────────────────────────────────┘
           │ Implements
┌──────────▼──────────────────────────────────────┐
│                PORT (Interface)                 │
│   Notification, Logger, Cache                  │
└──────────┬──────────────────────────────────────┘
           │ Adapter
└──────────▼──────────────────────────────────────┘
          Внешний мир (1/3 части)
   Notification Service, Cache Service

Ключевые концепции

1. HEXAGON (Core/Domain)

Это сердце приложения — бизнес-логика, полностью независимая от внешних сервисов.

// Domain Entity
public class User {
    private String id;
    private String email;
    private String name;
    private boolean active;
    
    public User(String id, String email, String name) {
        this.id = id;
        this.email = email;
        this.name = name;
        this.active = true;
    }
    
    public void deactivate() {
        this.active = false;
    }
}

// Use Case (бизнес-логика)
public class CreateUserUseCase {
    private final UserRepository repository;
    private final EmailValidator validator;
    private final EventPublisher publisher;
    
    public CreateUserUseCase(UserRepository repository, 
                            EmailValidator validator,
                            EventPublisher publisher) {
        this.repository = repository;
        this.validator = validator;
        this.publisher = publisher;
    }
    
    public User execute(CreateUserCommand cmd) throws ValidationException {
        // Валидация
        if (!validator.isValid(cmd.getEmail())) {
            throw new ValidationException("Invalid email");
        }
        
        // Создание
        User user = new User(UUID.randomUUID().toString(), 
                            cmd.getEmail(), 
                            cmd.getName());
        
        // Сохранение (через port)
        repository.save(user);
        
        // Уведомление (через port)
        publisher.publish(new UserCreatedEvent(user.getId()));
        
        return user;
    }
}

2. PORT (Interface/Contract)

Порт определяет контракт между доменом и внешним миром. Это интерфейсы, которые доменный код вызывает.

// INPUT PORT (Driven Port)
// Интерфейсы, которые домен ИСПОЛЬЗУЕТ
public interface UserRepository {
    void save(User user);
    Optional<User> findById(String id);
    void delete(String id);
}

public interface EmailValidator {
    boolean isValid(String email);
}

public interface EventPublisher {
    void publish(DomainEvent event);
}

// OUTPUT PORT (Driving Port)
// Интерфейсы, которые ИСПОЛЬЗУЮТ домен
public interface CreateUserPort {
    User createUser(CreateUserRequest request);
}

public interface GetUserPort {
    User getUser(String userId);
}

3. ADAPTER (Implementation)

Адаптер реализует PORT, подключая конкретную технологию (БД, REST API, etc).

// PRIMARY ADAPTER (REST)
@RestController
@RequestMapping("/api/users")
public class CreateUserAdapter implements CreateUserPort {
    private final CreateUserUseCase useCase;
    
    public CreateUserAdapter(CreateUserUseCase useCase) {
        this.useCase = useCase;
    }
    
    @PostMapping
    public ResponseEntity<UserResponse> create(@RequestBody CreateUserRequest request) {
        User user = useCase.execute(new CreateUserCommand(request));
        return ResponseEntity.ok(new UserResponse(user));
    }
}

// SECONDARY ADAPTER (PostgreSQL)
@Repository
public class PostgresUserRepository implements UserRepository {
    private final JdbcTemplate jdbc;
    
    @Override
    public void save(User user) {
        String sql = "INSERT INTO users (id, email, name, active) VALUES (?, ?, ?, ?)";
        jdbc.update(sql, user.getId(), user.getEmail(), user.getName(), user.isActive());
    }
    
    @Override
    public Optional<User> findById(String id) {
        String sql = "SELECT * FROM users WHERE id = ?";
        return jdbc.queryForObject(sql, (rs, rowNum) -> 
            new User(rs.getString("id"), rs.getString("email"), rs.getString("name")), 
            id
        ).map(Optional::of).orElse(Optional.empty());
    }
}

// SECONDARY ADAPTER (Email Validator)
@Component
public class RegexEmailValidator implements EmailValidator {
    private static final String EMAIL_PATTERN = 
        "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
    
    @Override
    public boolean isValid(String email) {
        return email.matches(EMAIL_PATTERN);
    }
}

// SECONDARY ADAPTER (Event Publisher)
@Component
public class KafkaEventPublisher implements EventPublisher {
    private final KafkaTemplate<String, String> kafka;
    
    @Override
    public void publish(DomainEvent event) {
        kafka.send("domain-events", JsonUtils.toJson(event));
    }
}

Полный пример: Order Service

// DOMAIN
public class Order {
    private String id;
    private String customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    
    public Order(String id, String customerId) {
        this.id = id;
        this.customerId = customerId;
        this.items = new ArrayList<>();
        this.status = OrderStatus.PENDING;
    }
    
    public void addItem(OrderItem item) {
        items.add(item);
    }
    
    public void confirm() throws OrderException {
        if (items.isEmpty()) {
            throw new OrderException("Cannot confirm empty order");
        }
        this.status = OrderStatus.CONFIRMED;
    }
}

// USE CASE
public class ConfirmOrderUseCase {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentProcessor paymentProcessor;
    private final OrderEventPublisher eventPublisher;
    
    public Order execute(String orderId) throws OrderException {
        Order order = orderRepository.findById(orderId)
            .orElseThrow(() -> new OrderNotFoundException("Order not found"));
        
        // Проверка наличия товара
        for (OrderItem item : order.getItems()) {
            if (!inventoryService.checkAvailable(item.getProductId(), item.getQuantity())) {
                throw new OutOfStockException(item.getProductId());
            }
        }
        
        // Подтверждение заказа
        order.confirm();
        
        // Резервирование товара
        order.getItems().forEach(item -> 
            inventoryService.reserve(item.getProductId(), item.getQuantity())
        );
        
        // Обработка платежа
        paymentProcessor.charge(order.getCustomerId(), order.getTotalPrice());
        
        // Сохранение
        orderRepository.save(order);
        
        // Публикация события
        eventPublisher.publish(new OrderConfirmedEvent(order.getId()));
        
        return order;
    }
}

// PORTS
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(String id);
}

public interface InventoryService {
    boolean checkAvailable(String productId, int quantity);
    void reserve(String productId, int quantity);
}

public interface PaymentProcessor {
    void charge(String customerId, double amount) throws PaymentException;
}

public interface OrderEventPublisher {
    void publish(DomainEvent event);
}

// ADAPTERS
@RestController
@RequestMapping("/api/orders")
public class OrderRestAdapter {
    private final ConfirmOrderUseCase useCase;
    
    @PostMapping("/{id}/confirm")
    public ResponseEntity<OrderResponse> confirm(@PathVariable String id) {
        Order order = useCase.execute(id);
        return ResponseEntity.ok(new OrderResponse(order));
    }
}

@Repository
public class JpaOrderRepository implements OrderRepository {
    private final OrderJpaRepository jpa;
    
    @Override
    public void save(Order order) {
        jpa.save(new OrderEntity(order));
    }
    
    @Override
    public Optional<Order> findById(String id) {
        return jpa.findById(id).map(OrderEntity::toDomain);
    }
}

@Service
public class RemoteInventoryAdapter implements InventoryService {
    private final RestTemplate rest;
    
    @Override
    public boolean checkAvailable(String productId, int quantity) {
        InventoryResponse response = rest.getForObject(
            "/inventory/" + productId, InventoryResponse.class
        );
        return response.getQuantity() >= quantity;
    }
    
    @Override
    public void reserve(String productId, int quantity) {
        rest.postForObject(
            "/inventory/" + productId + "/reserve",
            new ReserveRequest(quantity),
            Void.class
        );
    }
}

Преимущества гексагональной архитектуры

  1. Тестируемость — можно подменять адаптеры на mock'и
@Test
public void testCreateOrder() {
    // Mock adapters
    OrderRepository mockRepo = mock(OrderRepository.class);
    InventoryService mockInventory = mock(InventoryService.class);
    
    // Инжектируем в use case
    ConfirmOrderUseCase useCase = new ConfirmOrderUseCase(
        mockRepo, mockInventory, /* ... */
    );
    
    // Тест работает с чистой доменной логикой
    Order order = useCase.execute("123");
}
  1. Независимость от фреймворков — домен не знает о Spring, JPA и т.д.

  2. Легкость подмены технологий — хотим другую БД? Просто новый адаптер

  3. Четкое разделение ответственности — каждый слой имеет одну роль

  4. Дизайн driven по портам — интерфейсы определяют что нужно домену

Недостатки

  1. Больше code — много интерфейсов и адаптеров
  2. Complexity — может быть избыточна для маленьких приложений
  3. Learning curve — требует понимания архитектурных паттернов

Best Practices

  1. Домен не зависит от фреймворков
  2. Use Cases содержат бизнес-логику, не технические детали
  3. Ports определяют контракт, не реализацию
  4. Несколько адаптеров на один port - нормально (REST и gRPC на одну use case)
  5. Тестируй use cases с mock адаптерами