Hexagonal Architecture with Spring Boot

Hexagonal Architecture with Spring Boot

Part 1 — Why Hexagonal Architecture?

The vast majority of Spring Boot projects are built around the Controller → Service → Repository trio. This structure is sufficient for many scenarios. However, as a product grows and business rules become more complex, the limitations of this structure become apparent. In this article, we will examine why some teams turn to Hexagonal Architecture — not as a theoretical framework, but through the concrete problems encountered in day-to-day development.

Where does the classic structure fall short?

In a typical Spring project, OrderService contains @Transactional, OrderRepository extends JpaRepository, and the entity class combines both @Entity annotations and business rules. In this setup, a single class takes on three different responsibilities: representing a database table, conforming to JSON serialization, and carrying domain logic.

While this structure works well in the short term, it eventually leads to the following situations:

  • A field name change affects the DB migration, the API contract, and unit tests simultaneously.
  • When an existing endpoint also needs to be triggered via Kafka, the same service method must be called from a consumer as well, and transaction boundaries become blurred.
  • Validating a simple business rule requires starting up the Spring context. Tests running with @SpringBootTest lengthen both the local development loop and the CI duration.
  • Inbound port (driving): The interface defined for the outside world to issue commands to the application. For example, CreateOrderUseCase.
  • Outbound port (driven): The interface that defines the functionality the application needs from the outside. For example, LoadOrderPort, PublishOrderEventPort.

The core problem is clear: business logic is tightly bound to the framework and the database. The domain layer is expected to have no knowledge of JPA or Spring; in the current structure, this separation cannot be achieved.

The core proposal of Hexagonal Architecture

The central proposal of Hexagonal Architecture is a single idea: business logic is placed at the center, and every connection to the outside world is placed behind an interface.

The application consists of two regions. The inside contains the domain and application layers; these layers are written in plain Java or Kotlin and contain no frameworks or annotations. The outside contains the adapters: REST controllers, JPA repositories, Kafka consumers, external service clients.

The two regions communicate through interfaces called ports:

The critical rule of the model is the direction of dependencies: all dependencies flow from the outside inward. The controller depends on the use case interface; the use case calls the outbound port but does not know its implementation. The JPA adapter implements this port. As a result, the domain depends on no technology.

Benefits in modern Spring Boot projects

Recent developments in the Spring ecosystem have significantly increased the value of the Hexagonal approach.

Microservices and modular monoliths. If a service contains more than one bounded context, each context can have its own domain model. Maintaining this separation with code tightly coupled to a framework is difficult; with the port-adapter separation, boundaries are drawn much more clearly.

Event-driven architectures. With the widespread adoption of technologies such as Kafka, RabbitMQ, and SQS, services now typically include both a REST interface and at least one message consumer. When the same business logic needs to be triggered from multiple entry points, code duplication becomes inevitable without a use case layer.

Testability and fast feedback. In cloud-native environments, pipeline speed is a critical productivity metric. Being able to run domain tests in milliseconds without starting a Spring context noticeably accelerates the development loop.

Resilience to technology changes. Decisions such as moving from JPA to jOOQ, from REST to gRPC, or from a single database to a CQRS structure are on the agenda of many teams. When the domain is isolated, these transitions can be completed with a limited intervention; otherwise, a significant portion of the code needs to be rewritten.

Observability and cross-cutting concerns. Horizontal concerns such as tracing, metric collection, and structured logging should be placed at the adapter level. The Hexagonal approach defines a clear location for such functionality: the inner layers are left untouched, and these concerns are placed at the boundaries.

Should it be applied in every project?

No. For a CRUD service consisting of three endpoints, a short-lived prototype, or a small-scale internal tool, the additional cost introduced by this approach usually does not pay off. Additional interfaces, extra mapping layers, and a broader package structure increase the initial cost.

Hexagonal Architecture proves its value in systems where business rules are rich and long-term development is expected. Payments, orders, subscription management, pricing engines, and rule-based systems are the examples in which the return on this approach materializes fastest. From the point where the domain itself begins to require more attention than the framework, the investment starts to amortize itself.

The next part

In this part, we examined the practical value of Hexagonal Architecture. The next part will move to concrete steps: the concepts of port and adapter will be clearly defined, the example domain to be used throughout the series will be selected, and the package skeleton of the Spring Boot project will be established.


Spring Boot ile Hexagonal Mimari

Bölüm 1 — Neden Hexagonal Mimari?

Spring Boot ile geliştirilen projelerin büyük çoğunluğu Controller → Service → Repository üçlüsü üzerine kuruludur. Bu yapı pek çok senaryo için yeterlidir. Ancak ürün büyüdükçe ve iş kuralları karmaşıklaştıkça yapının sınırları belirgin hale gelir. Bu yazıda, bazı ekiplerin neden Hexagonal Mimari’ye yöneldiğini teorik bir çerçeve olarak değil, günlük geliştirme sürecinde karşılaşılan somut sorunlar üzerinden ele alacağız.

Klasik yapı nerede yetersiz kalır?

Tipik bir Spring projesinde OrderService içinde @Transactional, OrderRepository extends JpaRepository ve entity sınıfında hem @Entity anotasyonları hem iş kuralları bir arada bulunur. Bu yapıda tek bir sınıf üç farklı sorumluluğu üstlenir: veritabanı tablosunu temsil etmek, JSON serileştirmesine uygun olmak ve domain mantığını taşımak.

Kısa vadede sorunsuz çalışan bu yapı, zamanla şu tür durumlara yol açar:

  • Bir alan adının değiştirilmesi; DB migration, API kontratı ve birim testlerini aynı anda etkiler.
  • Mevcut bir endpoint’in Kafka üzerinden de tetiklenmesi gerektiğinde, aynı servis metodunun consumer’dan çağrılması kaçınılmaz hale gelir ve transaction sınırları belirsizleşir.
  • Basit bir iş kuralının doğrulanması için Spring context’inin ayağa kaldırılması gerekir. @SpringBootTest ile çalışan testler hem yerel geliştirme döngüsünü hem CI süresini uzatır.
  • Inbound port (driving): Dış dünyanın uygulamaya komut vermesi için tanımlanan arayüz. Örneğin CreateOrderUseCase.
  • Outbound port (driven): Uygulamanın dış dünyadan ihtiyaç duyduğu işlevleri tanımlayan arayüz. Örneğin LoadOrderPort, PublishOrderEventPort.

Temel sorun nettir: iş mantığı framework’e ve veritabanına sıkı biçimde bağlanmıştır. Domain katmanının JPA ya da Spring hakkında bilgi sahibi olmaması beklenir; mevcut yapıda bu ayrım sağlanamaz.

Hexagonal Mimari’nin temel önerisi

Hexagonal Mimari’nin merkezi önerisi tektir: iş mantığı ortaya alınır ve dış dünyayla olan her bağlantı bir arayüzün arkasına yerleştirilir.

Uygulama iki bölgeden oluşur. İçeride domain ve application katmanları bulunur; bu katmanlar saf Java ya da Kotlin ile yazılır, herhangi bir framework veya anotasyon içermez. Dışarıda ise adapter’lar yer alır: REST controller, JPA repository, Kafka consumer, harici servis istemcileri.

İki bölge, port adı verilen arayüzler üzerinden iletişim kurar:

Modelin kritik kuralı bağımlılık yönüdür: tüm bağımlılıklar dışarıdan içeriye doğru akar. Controller use case arayüzüne bağımlıdır; use case outbound port’u çağırır ancak port’un implementasyonunu tanımaz. JPA adapter bu port’u implemente eder. Sonuç olarak domain hiçbir teknolojiye bağımlı değildir.

Modern Spring Boot projelerinde sağladığı faydalar

Spring ekosistemindeki son dönem gelişmeler Hexagonal yaklaşımın değerini belirgin biçimde artırmıştır.

Mikroservisler ve modüler monolit. Bir servis içinde birden fazla bounded context varsa, her context’in kendi domain modeli bulunabilir. Framework’e sıkı bağlı bir kodla bu ayrımı korumak zordur; port-adapter ayrımıyla sınırlar çok daha net çizilir.

Event-driven mimariler. Kafka, RabbitMQ ve SQS gibi teknolojilerin yaygınlaşmasıyla servisler artık çoğunlukla hem REST hem de en az bir mesaj tüketicisine sahiptir. Aynı iş mantığının birden fazla giriş noktasından tetiklenmesi gerektiğinde, use case katmanı olmadan kod tekrarı kaçınılmaz hale gelir.

Test edilebilirlik ve hızlı geri bildirim. Cloud-native ortamlarda pipeline hızı kritik bir verimlilik ölçütüdür. Domain testlerinin Spring context’i olmadan milisaniyeler içinde çalıştırılabilmesi, geliştirme döngüsünü belirgin biçimde hızlandırır.

Teknoloji değişimine dayanıklılık. JPA’dan jOOQ’ya, REST’ten gRPC’ye veya tek veritabanından CQRS yapısına geçiş gibi kararlar pek çok ekibin gündemindedir. Domain izole edildiğinde bu geçişler sınırlı bir müdahaleyle tamamlanır; aksi halde kodun önemli bir kısmının yeniden yazılması gerekir.

Observability ve cross-cutting concerns. Tracing, metrik toplama ve yapılandırılmış loglama gibi yatay konular adapter seviyesinde konumlandırılmalıdır. Hexagonal yaklaşım bu tür işlevler için net bir yer tanımlar: iç katmanlara dokunulmaz, bu işlevler sınırlara yerleştirilir.

Her projede uygulanmalı mı?

Hayır. Üç endpoint’ten oluşan bir CRUD servisi, kısa ömürlü bir prototip veya küçük ölçekli bir internal tool için bu yaklaşımın getirdiği ek maliyet çoğunlukla karşılık bulmaz. Ek arayüzler, ek mapping katmanları ve daha geniş paket yapısı başlangıç maliyetini artırır.

Hexagonal Mimari, iş kurallarının zenginleştiği ve uzun süreli geliştirilmesi beklenen sistemlerde karşılığını verir. Ödeme, sipariş, abonelik yönetimi, fiyatlandırma motoru ve kural tabanlı sistemler bu yaklaşımın getirisini en hızlı gösteren örneklerdir. Domain’in kendisi framework’ten daha fazla dikkat gerektirmeye başladığı andan itibaren yatırım kendini amorti etmeye başlar.

Sonraki bölüm

Bu bölümde Hexagonal Mimari’nin pratik değerini ele aldık. Bir sonraki bölümde somut adımlara geçilecektir: port ve adapter kavramları net biçimde tanımlanacak, seri boyunca kullanılacak örnek domain seçilecek ve Spring Boot projesinin paket iskeleti oluşturulacaktır.


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