Skip to main content

Introduction

FoodTech Kitchen Service implements a Hexagonal Architecture (Ports & Adapters) following Clean Architecture principles. The system automates the decomposition of restaurant orders into station-specific tasks.
This architecture ensures complete separation of business logic from infrastructure concerns, making the system highly testable and maintainable.

The Problem We Solve

When a restaurant order contains multiple products (drinks, hot dishes, salads), the system automatically:
  1. Groups products by their kitchen station
  2. Creates specific preparation tasks for each station
  3. Executes preparation commands with station-appropriate workflows

BAR Station

Handles all beverages and cocktails
  • 3 seconds per drink
  • Parallel preparation possible

Hot Kitchen

Prepares hot dishes and soups
  • 7 seconds per dish
  • Sequential cooking required

Cold Kitchen

Assembles salads and desserts
  • 5 seconds per dish
  • Fresh ingredient handling

Architectural Layers

The system is organized into three concentric layers, each with distinct responsibilities:

Domain Layer (Core)

The innermost layer contains pure business logic with zero external dependencies.
Entities:
  • Order: Represents a customer order with table number and products
  • Task: Represents a station-specific preparation task with lifecycle
  • Product: Individual items with type (DRINK, HOT_DISH, COLD_DISH)
Commands:
  • PrepareDrinkCommand: Executes beverage preparation
  • PrepareHotDishCommand: Executes hot dish cooking
  • PrepareColdDishCommand: Executes cold dish assembly
Domain Services:
  • TaskDecomposer: Breaks orders into station-specific tasks
  • TaskFactory: Creates task instances
  • CommandFactory: Creates appropriate commands per station
  • OrderValidator: Enforces business rules

Application Layer (Orchestration)

Orchestrates business logic through use cases without knowing infrastructure details.
1

Define Input Ports

Interfaces that represent what the application can do:
public interface ProcessOrderPort {
    List<Task> execute(Order order);
}
2

Define Output Ports

Interfaces that represent what the application needs:
public interface OrderRepository {
    Order save(Order order);
}
3

Implement Use Cases

Orchestrate domain services to fulfill business requirements:
src/main/java/com/foodtech/kitchen/application/usecases/ProcessOrderUseCase.java
@Service
public class ProcessOrderUseCase implements ProcessOrderPort {
    private final OrderRepository orderRepository;
    private final TaskDecomposer taskDecomposer;
    private final TaskRepository taskRepository;

    @Override
    public List<Task> execute(Order order) {
        Order savedOrder = orderRepository.save(order);
        List<Task> tasks = taskDecomposer.decompose(savedOrder);
        taskRepository.saveAll(tasks);
        return tasks;
    }
}

Infrastructure Layer (Technical Details)

Provides concrete implementations of ports using specific technologies.

REST Adapter

Exposes HTTP endpoints for external clients
  • Maps DTOs to domain objects
  • Handles HTTP concerns (status codes, validation)

Persistence Adapter

Implements repository interfaces using JPA
  • Maps domain objects to JPA entities
  • Manages database transactions

Dependency Rule

Critical principle: Dependencies always point inward. Outer layers depend on inner layers, never the reverse.
┌─────────────────────────────────────┐
│     Infrastructure Layer            │ ──┐
│  (REST, JPA, Config)               │   │
└─────────────────────────────────────┘   │
          ↓ depends on                    │
┌─────────────────────────────────────┐   │
│     Application Layer               │   │ All dependencies
│  (Use Cases, Ports)                │   │ point INWARD
└─────────────────────────────────────┘   │
          ↓ depends on                    │
┌─────────────────────────────────────┐   │
│     Domain Layer                    │ ←─┘
│  (Entities, Commands, Services)    │
│  NO EXTERNAL DEPENDENCIES          │
└─────────────────────────────────────┘

Key Benefits

Testability

1

Unit Test Domain Logic

Test business logic in isolation without any infrastructure:
@Test
void shouldDecomposeOrderIntoTasks() {
    Order order = new Order("A1", List.of(
        new Product("Coca Cola", ProductType.DRINK),
        new Product("Pizza", ProductType.HOT_DISH)
    ));
    
    List<Task> tasks = taskDecomposer.decompose(order);
    
    assertEquals(2, tasks.size());
}
2

Integration Test Adapters

Test infrastructure components with real implementations
3

End-to-End Test API

Test complete flows through REST endpoints

Maintainability

  • Change database? Only modify persistence adapters
  • Switch from REST to GraphQL? Only modify API adapters
  • Add new station type? Extend domain with new command, no infrastructure changes

Technology Independence

The core business logic (Domain + Application layers) has zero knowledge of:
  • Spring Framework
  • JPA/Hibernate
  • HTTP/REST
  • Jackson (JSON)
  • H2 Database
This makes the core logic:
  • Portable across frameworks
  • Testable without containers
  • Evolvable without breaking changes

Request Flow Example

Let’s trace a complete order through all layers:
Notice how each layer performs its specific responsibility without knowledge of adjacent layers’ implementation details.

Project Structure

The physical file structure mirrors the logical architecture:
com.foodtech.kitchen/
├── domain/                    # Domain Layer - No dependencies
│   ├── model/
│   │   ├── Order.java
│   │   ├── Task.java
│   │   ├── Product.java
│   │   ├── Station.java
│   │   └── ProductType.java
│   ├── commands/
│   │   ├── Command.java
│   │   ├── PrepareDrinkCommand.java
│   │   ├── PrepareHotDishCommand.java
│   │   └── PrepareColdDishCommand.java
│   └── services/
│       ├── TaskDecomposer.java
│       ├── TaskFactory.java
│       └── CommandFactory.java

├── application/               # Application Layer - Depends on Domain
│   ├── usecases/
│   │   ├── ProcessOrderUseCase.java
│   │   └── GetTasksByStationUseCase.java
│   └── ports/
│       ├── in/                # Driving ports
│       │   └── ProcessOrderPort.java
│       └── out/               # Driven ports
│           ├── OrderRepository.java
│           └── TaskRepository.java

└── infrastructure/            # Infrastructure Layer - Depends on Application
    ├── rest/                  # REST Adapter (Driving)
    │   ├── OrderController.java
    │   └── dto/
    ├── persistence/           # JPA Adapter (Driven)
    │   ├── adapters/
    │   │   └── OrderRepositoryAdapter.java
    │   └── jpa/
    │       └── OrderJpaRepository.java
    └── config/
        └── ApplicationConfig.java

Design Principles Applied

Single Responsibility

Each class has one reason to change
  • OrderController: Handle HTTP
  • ProcessOrderUseCase: Orchestrate flow
  • OrderRepositoryAdapter: Persist data

Open/Closed

Open for extension, closed for modification
  • Add new station: Create new command
  • No changes to existing code

Dependency Inversion

Depend on abstractions, not concretions
  • Use cases depend on OrderRepository interface
  • Adapters implement the interface

Interface Segregation

Small, focused interfaces
  • ProcessOrderPort has one method
  • Clients don’t depend on unused methods

Next Steps

Hexagonal Details

Deep dive into ports and adapters pattern

Command Pattern

Learn how commands encapsulate station logic

Layer Interactions

Understand how layers communicate

Build docs developers (and LLMs) love