hexagonal-java
Hexagonal architecture (ports & adapters) for Java Spring Boot. Package structure, port definitions, use case implementation, adapter patterns, and testing strategy. Use when structuring or reviewing Java services.
Best use case
hexagonal-java is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Hexagonal architecture (ports & adapters) for Java Spring Boot. Package structure, port definitions, use case implementation, adapter patterns, and testing strategy. Use when structuring or reviewing Java services.
Teams using hexagonal-java should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/hexagonal-java/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How hexagonal-java Compares
| Feature / Agent | hexagonal-java | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Hexagonal architecture (ports & adapters) for Java Spring Boot. Package structure, port definitions, use case implementation, adapter patterns, and testing strategy. Use when structuring or reviewing Java services.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Hexagonal Architecture for Java / Spring Boot
Ports & Adapters architecture for testable, framework-independent Java services.
> **vs `ddd-java`:** This skill focuses on **package structure and dependency direction** — ports (interfaces), adapters (infrastructure), use cases, and DI wiring with Spring. Use `ddd-java` when you need **domain modeling** — how to design Value Objects, Entities, Aggregates, and Domain Events.
## When to Activate
- Structuring a new Java/Spring Boot service from scratch
- Reviewing whether domain logic has leaked into adapters (or vice versa)
- Deciding where a new class belongs in the package hierarchy
- Writing tests: determining what to mock and at which boundary
- Replacing an adapter (e.g., switching from JPA to JDBC) without touching domain
## Core Principle
**Dependency arrows always point inward — toward domain.**
```plantuml
@startuml
!include <C4/C4_Container>
System_Boundary(svc, "Your Service") {
Container(in_adapt, "Inbound Adapters", "Spring MVC / Kafka / CLI", "drive the app")
Container(in_port, "Input Ports", "Java Interfaces", "domain/port/in/")
Container(usecase, "Use Cases", "Spring @Service", "application/usecase/")
Container(domain, "Domain Model", "Pure Java", "domain/model/ — no framework")
Container(out_port, "Output Ports", "Java Interfaces", "domain/port/out/")
Container(out_adapt, "Outbound Adapters", "JPA / REST Clients", "driven by the app")
}
Rel_D(in_adapt, in_port, "calls")
Rel_D(in_port, usecase, "implemented by")
Rel_D(usecase, domain, "uses")
Rel_D(domain, out_port, "defines")
Rel_D(out_port, out_adapt, "implemented by")
@enduml
```
## Package Structure
```
src/main/java/com/example/app/
domain/
model/ # Market.java, Money.java, MarketStatus.java
port/
in/ # CreateMarketUseCase.java, ListMarketsUseCase.java
out/ # MarketRepository.java, NotificationPort.java
event/ # MarketCreatedEvent.java
application/
usecase/ # CreateMarketService.java, ListMarketsService.java
adapter/
in/
web/ # MarketController.java, CreateMarketRequest.java, MarketResponse.java
messaging/ # MarketEventConsumer.java
out/
persistence/ # JpaMarketRepository.java, MarketEntity.java, MarketMapper.java
client/ # NotificationClient.java
config/ # MarketConfig.java (@Configuration, bean wiring only)
```
## Domain Model — No Framework Dependencies
```java
// domain/model/Market.java
public class Market {
private final MarketId id;
private final String name;
private final String slug;
private MarketStatus status;
private Market(MarketId id, String name, String slug, MarketStatus status) {
this.id = id;
this.name = name;
this.slug = slug;
this.status = status;
}
// Factory method — domain logic, not Spring
public static Market create(String name, String slug) {
if (name == null || name.isBlank()) throw new InvalidMarketException("name required");
return new Market(null, name, slug, MarketStatus.DRAFT);
}
// Behavior methods
public Market publish() {
if (this.status != MarketStatus.DRAFT) throw new MarketAlreadyPublishedException(slug);
return new Market(id, name, slug, MarketStatus.ACTIVE);
}
public String name() { return name; }
public String slug() { return slug; }
public MarketStatus status() { return status; }
}
```
## Input Ports (Use Case Interfaces)
```java
// domain/port/in/CreateMarketUseCase.java
public interface CreateMarketUseCase {
Market create(CreateMarketCommand command);
}
// domain/port/in/CreateMarketCommand.java (command = validated input)
public record CreateMarketCommand(
@NonNull String name,
@NonNull String slug
) {}
```
## Output Ports (Repository/External Service Interfaces)
```java
// domain/port/out/MarketRepository.java
public interface MarketRepository {
Market save(Market market);
Optional<Market> findBySlug(String slug);
List<Market> findAllActive(Pageable pageable);
}
// domain/port/out/NotificationPort.java
public interface NotificationPort {
void notifyMarketCreated(Market market);
}
```
## Use Case Implementation
```java
// application/usecase/CreateMarketService.java
// @Transactional lives here — not in domain, not in adapter
@Transactional
public class CreateMarketService implements CreateMarketUseCase {
private final MarketRepository marketRepository; // output port
private final NotificationPort notificationPort; // output port
public CreateMarketService(
MarketRepository marketRepository,
NotificationPort notificationPort
) {
this.marketRepository = marketRepository;
this.notificationPort = notificationPort;
}
@Override
public Market create(CreateMarketCommand command) {
var market = Market.create(command.name(), command.slug());
var saved = marketRepository.save(market);
notificationPort.notifyMarketCreated(saved);
return saved;
}
}
```
## Inbound Adapter — REST Controller
```java
// adapter/in/web/MarketController.java
@RestController
@RequestMapping("/api/markets")
@Validated
class MarketController {
private final CreateMarketUseCase createMarket; // input port only
MarketController(CreateMarketUseCase createMarket) {
this.createMarket = createMarket;
}
@PostMapping
ResponseEntity<MarketResponse> create(@Valid @RequestBody CreateMarketRequest req) {
var command = new CreateMarketCommand(req.name(), req.slug());
var market = createMarket.create(command);
return ResponseEntity.status(HttpStatus.CREATED).body(MarketResponse.from(market));
}
}
// adapter/in/web/CreateMarketRequest.java
public record CreateMarketRequest(
@NotBlank @Size(max = 200) String name,
@NotBlank @Pattern(regexp = "[a-z0-9-]+") String slug
) {}
// adapter/in/web/MarketResponse.java
public record MarketResponse(String name, String slug, String status) {
public static MarketResponse from(Market market) {
return new MarketResponse(market.name(), market.slug(), market.status().name());
}
}
```
## Outbound Adapter — Persistence
```java
// adapter/out/persistence/JpaMarketRepository.java
@Repository
class JpaMarketRepository implements MarketRepository { // implements output port
private final MarketJpaRepository jpaRepo; // Spring Data interface
JpaMarketRepository(MarketJpaRepository jpaRepo) {
this.jpaRepo = jpaRepo;
}
@Override
public Market save(Market market) {
MarketEntity entity = MarketMapper.toEntity(market);
return MarketMapper.toDomain(jpaRepo.save(entity));
}
@Override
public Optional<Market> findBySlug(String slug) {
return jpaRepo.findBySlug(slug).map(MarketMapper::toDomain);
}
@Override
public List<Market> findAllActive(Pageable pageable) {
return jpaRepo.findByStatus(MarketStatus.ACTIVE, pageable)
.stream().map(MarketMapper::toDomain).toList();
}
}
// adapter/out/persistence/MarketEntity.java — JPA annotations stay here, not in domain
@Entity
@Table(name = "markets")
class MarketEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String name;
String slug;
@Enumerated(EnumType.STRING)
MarketStatus status;
}
// adapter/out/persistence/MarketJpaRepository.java
interface MarketJpaRepository extends JpaRepository<MarketEntity, Long> {
Optional<MarketEntity> findBySlug(String slug);
List<MarketEntity> findByStatus(MarketStatus status, Pageable pageable);
}
```
## Spring Bean Wiring
```java
// config/MarketConfig.java
@Configuration
class MarketConfig {
@Bean
CreateMarketUseCase createMarketUseCase(
MarketRepository marketRepository,
NotificationPort notificationPort
) {
return new CreateMarketService(marketRepository, notificationPort);
}
}
```
## Testing Strategy
### Unit Test — Use Case (fast, no Spring)
Mock output ports; test business logic in isolation:
```java
@ExtendWith(MockitoExtension.class)
class CreateMarketUseCaseTest {
@Mock MarketRepository marketRepository;
@Mock NotificationPort notificationPort;
@InjectMocks CreateMarketService createMarket;
@Test
void create_savesMarketAndNotifies() {
var command = new CreateMarketCommand("Test Market", "test-market");
given(marketRepository.save(any())).willAnswer(inv -> inv.getArgument(0));
var result = createMarket.create(command);
assertThat(result.name()).isEqualTo("Test Market");
verify(marketRepository).save(any());
verify(notificationPort).notifyMarketCreated(any());
}
@Test
void create_throwsException_whenNameIsBlank() {
assertThatThrownBy(() -> createMarket.create(new CreateMarketCommand("", "slug")))
.isInstanceOf(InvalidMarketException.class);
}
}
```
### Adapter Test — Web Layer (`@WebMvcTest`)
Mock input port; test HTTP concerns only:
```java
@WebMvcTest(MarketController.class)
class MarketControllerTest {
@Autowired MockMvc mockMvc;
@MockBean CreateMarketUseCase createMarket; // input port — not the use case impl
@Test
void create_returns201_withValidPayload() throws Exception {
given(createMarket.create(any())).willReturn(
Market.create("Test", "test-market")
);
mockMvc.perform(post("/api/markets")
.contentType(MediaType.APPLICATION_JSON)
.content("""{"name":"Test","slug":"test-market"}"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.name").value("Test"));
}
}
```
### Adapter Test — Persistence Layer (`@DataJpaTest`)
Test the JPA adapter against a real database schema:
```java
@DataJpaTest
@AutoConfigureTestDatabase(replace = NONE)
@Import(TestContainersConfig.class)
class JpaMarketRepositoryTest {
@Autowired MarketJpaRepository jpaRepo;
JpaMarketRepository repository;
@BeforeEach
void setUp() { repository = new JpaMarketRepository(jpaRepo); }
@Test
void save_andFindBySlug() {
var market = Market.create("Test", "test-slug");
repository.save(market);
assertThat(repository.findBySlug("test-slug")).isPresent();
}
}
```
## Anti-Patterns
### Domain Class Importing Spring or JPA Annotations
**Wrong:**
```java
// domain/model/Market.java
import org.springframework.stereotype.Component;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
@Entity
@Table(name = "markets")
@Component
public class Market { ... } // framework annotations pollute domain
```
**Correct:**
```java
// domain/model/Market.java
// No framework imports — plain Java only
public class Market {
private final MarketId id;
private final String name;
private MarketStatus status;
private Market(MarketId id, String name, MarketStatus status) { ... }
}
```
**Why:** JPA and Spring annotations in the domain couple business logic to persistence and the container, breaking testability without a running Spring context.
---
### Controller Injecting the Use Case Implementation Instead of the Port
**Wrong:**
```java
// adapter/in/web/MarketController.java
@RestController
class MarketController {
private final CreateMarketService createMarketService; // concrete class
MarketController(CreateMarketService createMarketService) {
this.createMarketService = createMarketService;
}
}
```
**Correct:**
```java
// adapter/in/web/MarketController.java
@RestController
class MarketController {
private final CreateMarketUseCase createMarket; // input port interface
MarketController(CreateMarketUseCase createMarket) {
this.createMarket = createMarket;
}
}
```
**Why:** Depending on the concrete service class instead of the port interface prevents mocking in `@WebMvcTest` and leaks application-layer details into the adapter.
---
### @Transactional Placed on Domain Model Methods
**Wrong:**
```java
// domain/model/Market.java
public class Market {
@Transactional // Spring annotation has no business being here
public Market publish() {
if (this.status != MarketStatus.DRAFT) throw new MarketAlreadyPublishedException(slug);
return new Market(id, name, slug, MarketStatus.ACTIVE);
}
}
```
**Correct:**
```java
// application/usecase/PublishMarketService.java
@Transactional // transaction boundary belongs on the use case
public class PublishMarketService implements PublishMarketUseCase {
public Market publish(MarketId id) {
var market = marketRepository.findById(id).orElseThrow();
var published = market.publish(); // pure domain call
return marketRepository.save(published);
}
}
```
**Why:** `@Transactional` is an infrastructure concern; placing it on the domain model introduces a Spring dependency and the wrong transactional scope.
---
### Use Case Importing JPA Entity or Repository Interface
**Wrong:**
```java
// application/usecase/CreateMarketService.java
import com.example.adapter.out.persistence.MarketEntity;
import com.example.adapter.out.persistence.MarketJpaRepository;
public class CreateMarketService implements CreateMarketUseCase {
private final MarketJpaRepository jpaRepository; // Spring Data leaks into application layer
}
```
**Correct:**
```java
// application/usecase/CreateMarketService.java
import com.example.domain.port.out.MarketRepository; // output port only
public class CreateMarketService implements CreateMarketUseCase {
private final MarketRepository marketRepository; // domain-defined interface
}
```
**Why:** Importing JPA artifacts into the use case couples the application layer to the persistence technology, breaking the ability to swap databases without touching business logic.
---
### Mapping Skipped — Returning JPA Entity from Use Case
**Wrong:**
```java
// application/usecase/ListMarketsService.java
public List<MarketEntity> listActive() { // JPA entity escapes the adapter
return jpaRepository.findByStatus(MarketStatus.ACTIVE, Pageable.unpaged());
}
```
**Correct:**
```java
// application/usecase/ListMarketsService.java
public List<Market> listActive(Pageable pageable) {
return marketRepository.findAllActive(pageable); // returns domain objects via output port
}
// adapter/out/persistence/JpaMarketRepository.java — mapper lives here
public List<Market> findAllActive(Pageable pageable) {
return jpaRepo.findByStatus(MarketStatus.ACTIVE, pageable)
.stream().map(MarketMapper::toDomain).toList();
}
```
**Why:** Allowing JPA entities to escape the persistence adapter exposes database schema details to callers and makes schema changes break the entire codebase.
## Architecture Violation Checklist
- [ ] Domain classes import `org.springframework.*` → **violation**
- [ ] Domain classes import `javax.persistence.*` / `jakarta.persistence.*` → **violation**
- [ ] Controller imports a use case implementation class (not interface) → **violation**
- [ ] Use case imports `MarketEntity` or `MarketJpaRepository` → **violation**
- [ ] `adapter/in/` imports `adapter/out/` → **violation**
- [ ] `@Transactional` on domain model methods → **violation** (belongs in use case)
## When Hexagonal is Worth the Overhead
- Domain logic is complex (multiple invariants, domain events, aggregate roots)
- Multiple inbound adapters are likely (REST + CLI + message consumer)
- Persistence technology may change (JPA → JDBC → MongoDB)
- High test coverage of domain logic is required without starting Spring
- Team practices TDD and wants pure-Java unit tests
## Relationship to DDD
Hexagonal architecture is the **structural container**; DDD provides the **modeling substance**:
| Hexagonal | DDD |
|---|---|
| `domain/model/` | Entities, Value Objects, Aggregates |
| `domain/port/out/` | Repository interface (per Aggregate Root) |
| `domain/event/` | Domain Events (raised inside Aggregates) |
| `domain/service/` | Domain Services (stateless, no framework) |
| `application/usecase/` | Application Services (orchestrate, dispatch events) |
| `adapter/out/persistence/` | JPA entities + mappers (NOT domain entities) |
**Neither is complete without the other**: Hexagonal without DDD produces anemic models with behavior in use cases. DDD without hexagonal produces framework-coupled domain objects.
For DDD modeling patterns (Value Objects, Aggregates, Domain Services, Domain Events), see skill: `ddd-java`.Related Skills
java-testing
Java testing patterns: JUnit 5, Mockito, AssertJ, Testcontainers for integration tests, and coverage with JaCoCo. Core TDD methodology for plain Java projects (non-Spring). For Spring Boot, see springboot-tdd.
java-patterns
Java coding standards and idioms for Java 25+ — naming, immutability, Optional, streams, exceptions, generics, records, sealed classes. Applies to plain Java, Spring Boot, Quarkus, and Jakarta EE projects.
hexagonal-typescript
Hexagonal architecture (ports & adapters) for TypeScript Node.js backends. Package structure, port definitions, use case implementation, adapter patterns, DI wiring, and testing strategy. Use when structuring or reviewing TypeScript backend services.
hexagonal-typescript-advanced
Advanced Hexagonal Architecture anti-patterns for TypeScript — domain importing framework dependencies, use cases depending on concrete adapters, HTTP handlers bypassing use cases, Zod validation inside the domain model. Each anti-pattern includes wrong/correct comparison with explanation.
ddd-java
Domain-Driven Design tactical patterns for Java 25+. Value Objects, Entities, Aggregates, Domain Services, Domain Events, Ubiquitous Language, and Bounded Contexts. Use when modeling domain logic in Java Spring Boot services.
zero-trust-patterns
Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.
wireframing
Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.
webrtc-patterns
WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.
webhook-patterns
Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.
web-performance
Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.
wasm-performance
WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer.
wasm-patterns
WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).