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