Hexagonal Architecture with Spring Boot Part 2

Hexagonal Architecture with Spring Boot

Part 2 — Port, Adapter, Use Case: Where Does Each File Go?

In the first part, the practical reasons for adopting Hexagonal Architecture were discussed. This article moves on to concrete steps: the example application that will be used throughout the series will be introduced, the terms port / adapter / use case will be clearly defined, and the package skeleton of the Spring Boot project will be established. No code will be written yet; however, by the end of this part, it will be clear where each file belongs within the project.

The example application: a subscription service

The series will use a subscription management service as its running example. This choice ensures that the subject neither remains at a trivial CRUD level nor becomes complex enough to overshadow the examples. The core business rules are as follows:

  • A user can subscribe to a plan.
  • The subscription renews at the beginning of each billing period; if payment fails, it is suspended.
  • The user can cancel at any time; the cancellation takes effect at the beginning of the next billing period.
  • When the plan changes, the price difference is calculated proportionally.
  • Inbound port: The interface defined for the outside world to issue commands to the application; it is the use case itself. The controller calls this interface.
  • Outbound port: The interface that expresses the functionality the application needs from the outside world. Operations such as “save the subscription,” “charge payment,” and “send email” fall into this category. The use case calls them.
  • Inbound adapter: REST controller, Kafka consumer, scheduled job.
  • Outbound adapter: JPA repository implementation, Stripe client, SendGrid client.
  • Inbound port → ...UseCase (e.g., CancelSubscriptionUseCase)
  • Outbound port → ...Port (e.g., LoadSubscriptionPort)
  • Use case implementation → ...Service (e.g., CancelSubscriptionService)
  • Inbound adapter → ...Controller, ...Consumer, ...Scheduler
  • Outbound adapter → ...Adapter (e.g., SubscriptionPersistenceAdapter)

These rules form the core of the domain. Technical components such as the database, REST, and the payment provider are positioned as the connection points between these rules and the outside world.

Three terms, three responsibilities

Use case is a unit of work the application exposes to the outside. It represents a single user-observable intent: “cancel the subscription,” “upgrade the plan,” “process the payment result.” Each use case is defined as an interface; its implementation is a service in the application layer.

SubscribeUserUseCase // interface (inbound port)

SubscribeUserService // implementation

 

Port is divided into two types:

LoadSubscriptionPort // outbound

SaveSubscriptionPort // outbound

ChargePaymentPort // outbound

 

Adapter is the concrete implementation of a port; it represents the technology-bound part.

Summary formula: use case = work, port = interface, adapter = technology.

Package skeleton

The recommended structure for a single-module Spring Boot project is shown below. A multi-module approach (Gradle subprojects) is also possible; however, it usually introduces more complexity than necessary at the initial stage.

com.example.subscription

├── domain

│ ├── model

│ │ ├── Subscription.java

│ │ ├── Plan.java

│ │ └── BillingPeriod.java

│ └── service // domain services, if any

├── application

│ ├── port

│ │ ├── in // inbound ports = use case interfaces

│ │ │ ├── SubscribeUserUseCase.java

│ │ │ └── CancelSubscriptionUseCase.java

│ │ └── out // outbound ports

│ │ ├── LoadSubscriptionPort.java

│ │ ├── SaveSubscriptionPort.java

│ │ └── ChargePaymentPort.java

│ └── service

│ ├── SubscribeUserService.java

│ └── CancelSubscriptionService.java

├── adapter

│ ├── in

│ │ ├── web // REST

│ │ │ ├── SubscriptionController.java

│ │ │ └── dto/

│ │ └── messaging // Kafka consumer, etc.

│ └── out

│ ├── persistence // JPA

│ │ ├── SubscriptionPersistenceAdapter.java

│ │ ├── SubscriptionJpaEntity.java

│ │ └── SubscriptionJpaRepository.java

│ ├── payment // Stripe

│ │ └── StripePaymentAdapter.java

│ └── notification // SendGrid

│ └── EmailNotificationAdapter.java

└── SubscriptionApplication.java

Naming convention

Consistent naming significantly improves readability. The following convention will be applied throughout the series:

When looking at a class name, the layer it belongs to and the direction it faces can be understood immediately. This provides a notable advantage during code reviews and when onboarding new developers to the project.

Dependency rule — the one-sentence summary

At the code level, the only rule to preserve is this: adapters may depend on application, application may depend on domain. Imports in the reverse direction are not accepted.

Catching these violations by eye in the IDE is difficult. In Part 8, this rule will be enforced automatically with ArchUnit; at the current stage, preserving the rule conceptually is sufficient.

The next part

In this part, the package skeleton was established, the naming convention was defined, and the example domain to be used throughout the series was selected. In the next part, the domain layer will begin to be written: the Subscription, Plan, and BillingPeriod classes. These classes will contain no framework code or annotations and will be written in plain Java. Unit tests will run in milliseconds without starting a Spring context. The first lines of code start at this point.


Spring Boot ile Hexagonal Mimari

Bölüm 2 — Port, Adapter, Use Case: Hangi Dosya Nereye Gider?

İlk bölümde Hexagonal Mimari’ye geçişin pratik nedenleri ele alındı. Bu yazıda somut adımlara geçilecektir: seri boyunca kullanılacak örnek uygulama tanıtılacak, port / adapter / use case terimleri net biçimde tanımlanacak ve Spring Boot projesinin paket iskeleti oluşturulacaktır. Kod henüz yazılmayacaktır; ancak bu bölümün sonunda hangi dosyanın projenin hangi noktasına yerleşeceği netleşmiş olacaktır.

Örnek uygulama: abonelik servisi

Seri boyunca bir abonelik yönetim servisi üzerinden ilerlenecektir. Bu tercih, konunun ne salt bir CRUD düzeyinde kalmasına ne de örnekleri gölgede bırakacak kadar karmaşıklaşmasına izin vermemek amacıyla yapılmıştır. Çekirdek iş kuralları şunlardır:

  • Kullanıcı bir plana abone olabilir.
  • Abonelik her dönem başında yenilenir; ödeme başarısız olursa askıya alınır.
  • Kullanıcı istediği zaman iptal edebilir; iptal bir sonraki dönem başında geçerli olur.
  • Plan değişikliğinde fiyat farkı orantılı olarak hesaplanır.
  • Inbound port: Dış dünyanın uygulamaya komut vermesi için tanımlanan arayüzdür; use case’in kendisidir. Controller bu arayüzü çağırır.
  • Outbound port: Uygulamanın dış dünyadan ihtiyaç duyduğu işlevleri ifade eden arayüzdür. “Aboneliği kaydet”, “ödeme al”, “e-posta gönder” gibi işlevler bu kategoriye girer. Use case bunları çağırır.
  • Inbound adapter: REST controller, Kafka consumer, scheduled job.
  • Outbound adapter: JPA repository implementasyonu, Stripe istemcisi, SendGrid istemcisi.
  • Inbound port → ...UseCase (ör. CancelSubscriptionUseCase)
  • Outbound port → ...Port (ör. LoadSubscriptionPort)
  • Use case implementasyonu → ...Service (ör. CancelSubscriptionService)
  • Inbound adapter → ...Controller, ...Consumer, ...Scheduler
  • Outbound adapter → ...Adapter (ör. SubscriptionPersistenceAdapter)

Bu kurallar domain’in çekirdeğini oluşturur. Veritabanı, REST ve ödeme sağlayıcısı gibi teknik bileşenler, bu kuralların dış dünyayla bağlantı noktaları olarak konumlanır.

Üç terim, üç sorumluluk

Use case uygulamanın dışarıya sunduğu bir iş birimidir. Kullanıcı tarafından gözlenen tek bir niyeti temsil eder: “aboneliği iptal et”, “planı yükselt”, “ödeme sonucunu işle”. Her use case bir arayüz olarak tanımlanır; implementasyonu application katmanındaki servistir.

SubscribeUserUseCase // arayüz (inbound port)

SubscribeUserService // implementasyon

 

Port iki türe ayrılır:

LoadSubscriptionPort // outbound

SaveSubscriptionPort // outbound

ChargePaymentPort // outbound

 

Adapter port’un somut implementasyonudur; teknolojiye bağlı kısmı temsil eder.

Özet formül: use case = iş, port = arayüz, adapter = teknoloji.

Paket iskeleti

Tek modüllü bir Spring Boot projesi için önerilen yapı aşağıdadır. Multi-module yaklaşımı (Gradle subprojects) da mümkündür; ancak başlangıç aşaması için genellikle gereğinden fazla karmaşıklık getirir.

com.example.subscription

├── domain

│ ├── model

│ │ ├── Subscription.java

│ │ ├── Plan.java

│ │ └── BillingPeriod.java

│ └── service // varsa domain servisleri

├── application

│ ├── port

│ │ ├── in // inbound port = use case arayüzleri

│ │ │ ├── SubscribeUserUseCase.java

│ │ │ └── CancelSubscriptionUseCase.java

│ │ └── out // outbound port'lar

│ │ ├── LoadSubscriptionPort.java

│ │ ├── SaveSubscriptionPort.java

│ │ └── ChargePaymentPort.java

│ └── service

│ ├── SubscribeUserService.java

│ └── CancelSubscriptionService.java

├── adapter

│ ├── in

│ │ ├── web // REST

│ │ │ ├── SubscriptionController.java

│ │ │ └── dto/

│ │ └── messaging // Kafka consumer vb.

│ └── out

│ ├── persistence // JPA

│ │ ├── SubscriptionPersistenceAdapter.java

│ │ ├── SubscriptionJpaEntity.java

│ │ └── SubscriptionJpaRepository.java

│ ├── payment // Stripe

│ │ └── StripePaymentAdapter.java

│ └── notification // SendGrid

│ └── EmailNotificationAdapter.java

└── SubscriptionApplication.java

İsimlendirme kuralı

Tutarlı isimlendirme, okunabilirliği belirgin biçimde artırır. Bu seride aşağıdaki kural uygulanacaktır:

Bir sınıf adına bakıldığında hangi katmanda olduğu ve hangi yöne baktığı doğrudan anlaşılabilir. Bu, kod incelemelerinde ve yeni geliştiricilerin projeye uyum sürecinde önemli bir kolaylık sağlar.

Bağımlılık kuralı — tek cümlelik özet

Kod düzeyinde korunması gereken tek kural şudur: adapter’lar application’a bağımlı olabilir, application domain’e bağımlı olabilir. Ters yönde import kabul edilmez.

IDE üzerinde bu ihlalleri gözle yakalamak güçtür. 8. bölümde ArchUnit ile bu kural otomatik olarak zorlanacaktır; mevcut aşamada kuralın kavramsal düzeyde korunması yeterlidir.

Sonraki bölüm

Bu bölümde paket iskeleti oluşturuldu, isimlendirme kuralı belirlendi ve seri boyunca kullanılacak örnek domain seçildi. Bir sonraki bölümde domain katmanı yazılmaya başlanacaktır: Subscription, Plan ve BillingPeriod sınıfları. Bu sınıflar framework veya anotasyon içermeyecek, saf Java ile yazılacaktır. Birim testleri ise Spring context’i olmadan milisaniyeler içinde çalıştırılabilecektir. İlk kod satırları bu noktada başlayacaktır.

 

This article was prepared by İlknur Bişirci.

Post Your Comment