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.
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:
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 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.
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.
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.
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.
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:
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 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.
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.
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.
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 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:
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.
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.
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
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.
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.
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.
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:
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.
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.
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
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.
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.
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.