Java'da Clean Architecture: Domain Mantığını Framework'ten Ayırmak

Java'da Clean Architecture: Domain Mantığını Framework'ten Ayırmak
https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html
Zelkulon14 Mart 20251 dk okuma

Robert C. Martin'in Clean Architecture'ı Java/Spring Boot'ta tutarlı şekilde uygulandı: katmanlar, bağımlılıklar ve domain'in neden Spring'den habersiz olması gerektiği.

Dört Katman

            ┌──────────────────────────────────┐
            │        Frameworks & Drivers      │  (Spring, JPA, REST)
            │  ┌─────────────────────────────┐ │
            │  │    Interface Adapters       │ │  (Controller'lar, Repository'ler)
            │  │  ┌───────────────────────┐  │ │
            │  │  │   Application/UseCases│  │ │  (İş Akışı)
            │  │  │  ┌─────────────────┐  │  │ │
            │  │  │  │     Domain      │  │  │ │  (Varlıklar, Kurallar)
            │  │  │  └─────────────────┘  │  │ │
            │  │  └───────────────────────┘  │ │
            │  └─────────────────────────────┘ │
            └──────────────────────────────────┘

Bağımlılık kuralı: oklar HER ZAMAN içe doğru gösterir.
Domain, Spring'i de JPA'yı da tanımaz.

Proje Yapısı

order-service/
└── src/main/java/com/zelkulon/order/
    ├── domain/                    ← En iç katman (Spring yok!)
    │   ├── model/
    │   │   └── Order.java
    │   └── valueobject/
    │       └── Money.java
    │
    ├── application/               ← Use Case'ler
    │   ├── port/
    │   │   ├── in/                ← Gelen portlar
    │   │   │   └── PlaceOrderUseCase.java
    │   │   └── out/               ← Giden portlar
    │   │       └── OrderRepository.java
    │   └── service/
    │       └── PlaceOrderService.java
    │
    └── adapter/                   ← En dış katman
        ├── in/web/
        │   └── OrderController.java
        └── out/persistence/
            ├── OrderJpaEntity.java  ← Yalnızca burada JPA!
            └── OrderPersistenceAdapter.java

Domain: Spring'siz

// Spring anotasyonu YOK, JPA YOK!
public class Order {

    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderItem> items;
    private OrderStatus status;

    public Order(OrderId id, CustomerId customerId, List<OrderItem> items) {
        if (items == null || items.isEmpty())
            throw new OrderException("Mindestens ein Artikel nötig");
        this.id     = id;
        this.items  = List.copyOf(items);
        this.status = OrderStatus.PENDING;
    }

    // Domain mantığı domain'de olmalı!
    public void confirm() {
        if (this.status != OrderStatus.PENDING)
            throw new OrderException("Nur PENDING kann bestätigt werden");
        this.status = OrderStatus.CONFIRMED;
    }
}

// Value Object (Record)
public record Money(BigDecimal amount, Currency currency) {
    public Money {
        if (amount.compareTo(BigDecimal.ZERO) < 0)
            throw new IllegalArgumentException("Betrag darf nicht negativ sein");
    }
    public Money add(Money other) {
        return new Money(amount.add(other.amount), currency);
    }
}

Port'lar ve Use Case

// Gelen port (Use Case Arayüzü)
public interface PlaceOrderUseCase {
    OrderId placeOrder(PlaceOrderCommand command);
}

// Use Case Uygulaması
@UseCase  // özel anotasyon, @Service'ten anlamlı
@Transactional
@RequiredArgsConstructor
public class PlaceOrderService implements PlaceOrderUseCase {

    private final OrderRepository orderRepository;   // Arayüz!
    private final PaymentGateway  paymentGateway;    // Arayüz!

    @Override
    public OrderId placeOrder(PlaceOrderCommand command) {
        var order = new Order(
            OrderId.newId(), command.customerId(), mapItems(command)
        );
        OrderId saved = orderRepository.save(order);
        paymentGateway.initiatePayment(order);
        return saved;
    }
}

Bağımlılık Tersine Çevirme

Clean Architecture olmadan:            Clean Architecture ile:

Controller                          Controller
    │                                   │
    ▼                                   ▼ (uygular)
OrderService ◄── JPA/DB            PlaceOrderUseCase (Port)
    │                                   ▲ (uygular)
    ▼                                   │
JpaRepository                       PlaceOrderService
                                        │
                                    OrderRepository (Port)
                                        ▲ (uygular)
                                        │
                                    OrderPersistenceAdapter
                                        │
                                    JpaRepository

Özet

  • Test edilebilirlik: Domain ve use case'ler Spring'siz test edilebilir (saf JUnit)
  • Sürdürülebilirlik: Veritabanı değişiklikleri domain'i etkilemez
  • Değiştirilebilirlik: Framework göçü domain değişikliği gerektirmez
  • Okunabilirlik: Her sınıfın net bir sorumluluğu vardır
  • Uzun ömür: İyi yapılandırılmış projeler yıllarca sürdürülebilir kalır
Java'da Clean Architecture: Domain Mantığını Framework'ten Ayırmak