Clean Architecture

Introduktion

Clean Architecture är ett arkitekturmönster introducerat av Robert C. Martin (“Uncle Bob”). Målet är att skapa system där affärslogiken är helt oberoende av ramverk, databaser, UI och externa tjänster. Beroenden pekar alltid inåt mot kärnan — aldrig utåt.


Beroenderegeln

Den fundamentala regeln i Clean Architecture:

“Källkodsberoenden får bara peka inåt — mot policies på högre nivå.”

Inget i en inre cirkel behöver veta något om en yttre cirkel.

┌──────────────────────────────────────────┐
│           Frameworks & Drivers           │  ← Webb, DB, UI
│  ┌──────────────────────────────────┐    │
│  │       Interface Adapters         │    │  ← Controllers, Presenters, Gateways
│  │  ┌──────────────────────────┐    │    │
│  │  │    Application Business  │    │    │  ← Use Cases
│  │  │  ┌────────────────────┐  │    │    │
│  │  │  │  Enterprise Rules  │  │    │    │  ← Entities (Affärslogik)
│  │  │  └────────────────────┘  │    │    │
│  │  └──────────────────────────┘    │    │
│  └──────────────────────────────────┘    │
└──────────────────────────────────────────┘

De fyra lagren

1. Entities — Affärsregler

Innersta kärnan. Innehåller universella affärsregler som inte ändras om UI eller databas byts ut.

// En Entity vet ingenting om databaser, HTTP eller ramverk
public class Order {
    private final String id;
    private final List<OrderItem> items;
    private OrderStatus status;

    public Order(String id, List<OrderItem> items) {
        if (items == null || items.isEmpty()) {
            throw new IllegalArgumentException("En order måste ha minst ett artikel.");
        }
        this.id     = id;
        this.items  = List.copyOf(items);
        this.status = OrderStatus.PENDING;
    }

    public Money calculateTotal() {
        return items.stream()
            .map(OrderItem::subtotal)
            .reduce(Money.ZERO, Money::add);
    }

    public void confirm() {
        if (this.status != OrderStatus.PENDING) {
            throw new IllegalStateException("Kan bara bekräfta en väntande order.");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    public String getId()          { return id; }
    public OrderStatus getStatus() { return status; }
}

2. Use Cases — Applikationslogik

Innehåller applikationsspecifik affärslogik. Orkestrerar entiteter för att uppnå ett specifikt mål.

// Use Case: Placera en order
public class PlaceOrderUseCase {

    private final OrderRepository  orderRepository;   // Interface, inte konkret klass!
    private final ProductRepository productRepository;
    private final EventPublisher   eventPublisher;

    public PlaceOrderUseCase(
        OrderRepository orderRepository,
        ProductRepository productRepository,
        EventPublisher eventPublisher
    ) {
        this.orderRepository   = orderRepository;
        this.productRepository = productRepository;
        this.eventPublisher    = eventPublisher;
    }

    public PlaceOrderResult execute(PlaceOrderCommand command) {
        // 1. Validera att produkterna finns
        List<OrderItem> items = command.getProductIds().stream()
            .map(productId -> {
                Product product = productRepository.findById(productId)
                    .orElseThrow(() -> new ProductNotFoundException(productId));
                return new OrderItem(product, command.getQuantity(productId));
            })
            .collect(Collectors.toList());

        // 2. Skapa ordern via domänentiteten
        Order order = new Order(UUID.randomUUID().toString(), items);

        // 3. Spara
        orderRepository.save(order);

        // 4. Publicera event
        eventPublisher.publish(new OrderPlacedEvent(order.getId(), order.calculateTotal()));

        return new PlaceOrderResult(order.getId(), order.calculateTotal());
    }
}

3. Interface Adapters — Konverteringslagret

Konverterar data mellan Use Case-format och yttre format (HTTP, databas).

// Controller — konverterar HTTP-förfrågan till Use Case-anrop
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    private final PlaceOrderUseCase placeOrderUseCase;

    @PostMapping
    public ResponseEntity<OrderResponse> placeOrder(@RequestBody PlaceOrderRequest request) {
        PlaceOrderCommand command = PlaceOrderCommand.from(request); // Konvertera
        PlaceOrderResult  result  = placeOrderUseCase.execute(command);
        return ResponseEntity.status(201).body(OrderResponse.from(result)); // Konvertera
    }
}

// Repository Adapter — implementerar domänens interface med JPA
@Repository
public class JpaOrderRepository implements OrderRepository {

    private final SpringDataOrderRepo jpaRepo;

    @Override
    public void save(Order order) {
        jpaRepo.save(OrderEntity.from(order)); // Domänobjekt → DB-entitet
    }

    @Override
    public Optional<Order> findById(String id) {
        return jpaRepo.findById(id).map(OrderEntity::toDomain); // DB-entitet → Domänobjekt
    }
}

4. Frameworks & Drivers — Det yttersta lagret

Spring Boot, JPA, HTTP-server, meddelandeköer. Dessa är detaljer — de kan bytas ut.


Projektstruktur

src/
├── domain/                     ← Entities & Value Objects
│   ├── model/
│   │   ├── Order.java
│   │   └── OrderItem.java
│   └── repository/             ← Interface (bara kontrakt)
│       └── OrderRepository.java

├── application/                ← Use Cases
│   ├── commands/
│   │   └── PlaceOrderCommand.java
│   ├── results/
│   │   └── PlaceOrderResult.java
│   └── usecases/
│       └── PlaceOrderUseCase.java

├── adapters/                   ← Interface Adapters
│   ├── web/
│   │   └── OrderController.java
│   └── persistence/
│       └── JpaOrderRepository.java

└── infrastructure/             ← Frameworks & Config
    ├── SpringConfig.java
    └── KafkaConfig.java

Fördelar

  • Testbarhet — Use Cases och Entities kan testas utan databas, HTTP eller ramverk.
  • Oberoende av ramverk — Byt ut Spring mot Quarkus utan att röra affärslogiken.
  • Oberoende av databas — Byt SQL mot MongoDB utan att ändra Use Cases.
  • Långsiktig underhållbarhet — Affärslogiken skyddas från teknisk skuld.

Nackdelar

  • Mer kod — Fler lager och interfaces ökar boilerplate.
  • Inlärningskurva — Teamet måste förstå varför gränser dras som de gör.
  • Kan vara överkurs — För CRUD-applikationer utan komplex affärslogik är det tungt.

Av Victor Hernandez från Bytebase.se