skills/quarkus-ddd/SKILL.md
Scaffold and generate Domain-Driven Design components with Hexagonal Architecture in Quarkus projects. Use this skill whenever the user wants to create a new bounded context, add an aggregate, create value objects, scaffold a DDD module, add a new subdomain, or generate any DDD tactical pattern (aggregate, entity, value object, command, event, repository, service, endpoint) in a Quarkus/Java project. Also trigger when the user mentions 'hexagonal architecture', 'ports and adapters', or asks to add a new feature following DDD patterns.
npx skillsauth add jeremyrdavis/claude-skill-quarkus-ddd quarkus-dddInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold bounded contexts and DDD tactical patterns in Quarkus projects using Hexagonal Architecture (Ports and Adapters).
When the user asks to create DDD components, first gather the essentials:
conference)sessions, attendees, speakers)Session, Attendee)Address, TimeSlot)RegisterAttendee, CreateSession)AttendeeRegistered, SessionCreated)Then ask:
How would you like to proceed?
- Full scaffolding — Generate all layers at once (domain, infrastructure, persistence)
- Layer by layer — Walk through each layer step by step, discussing decisions along the way
Proceed based on their choice.
Default to:
PanacheRepository)@Channel, Emitter)If the user hasn't specified a Java version, ask which version they'd like to use and suggest 25 as the default.
If the user requests a different stack (e.g., Reactive Hibernate, RabbitMQ, Active Record pattern), adapt accordingly.
Strict hierarchy — all bounded contexts follow this exact structure:
{basepackage}
└── {boundedcontext}
└── {subdomain}
├── domain
│ ├── aggregates ← Aggregate root classes
│ ├── events ← Domain event records
│ ├── services ← Application service, commands, result records
│ └── valueobjects ← Value object records
├── infrastructure ← REST endpoints, DTOs, event publishers
└── persistence ← JPA entities, Panache repositories
Example: for a Session aggregate in the conference.sessions subdomain:
conference.sessions.domain.aggregates.Session
conference.sessions.domain.events.SessionCreatedEvent
conference.sessions.domain.services.SessionService
conference.sessions.domain.services.CreateSessionCommand
conference.sessions.domain.services.SessionCreationResult
conference.sessions.domain.valueobjects.TimeSlot
conference.sessions.infrastructure.SessionEndpoint
conference.sessions.infrastructure.SessionDTO
conference.sessions.infrastructure.SessionEventPublisher
conference.sessions.persistence.SessionEntity
conference.sessions.persistence.SessionRepository
conference.sessions.persistence.TimeSlotEntity
| Role | Pattern | Example |
|------|---------|--------|
| Aggregate | {Noun} | Session |
| Value Object | {Noun} | TimeSlot |
| JPA Entity | {Noun}Entity | SessionEntity |
| Repository | {Noun}Repository | SessionRepository |
| Service | {Noun}Service | SessionService |
| Endpoint | {Noun}Endpoint | SessionEndpoint |
| DTO | {Noun}DTO | SessionDTO |
| Command | {Verb}{Noun}Command | CreateSessionCommand |
| Domain Event | {Noun}{PastTense}Event | SessionCreatedEvent |
| Result | {Noun}{Action}Result | SessionCreationResult |
| Event Publisher | {Noun}EventPublisher | SessionEventPublisher |
When doing full scaffolding, generate files in this order (domain first, adapters last):
domain/valueobjects/)Java records with compact constructor validation.
package {basepackage}.{boundedcontext}.{subdomain}.domain.valueobjects;
public record TimeSlot(LocalDateTime startTime, LocalDateTime endTime) {
public TimeSlot {
if (startTime == null) {
throw new IllegalArgumentException("Start time cannot be null");
}
if (endTime == null) {
throw new IllegalArgumentException("End time cannot be null");
}
if (!endTime.isAfter(startTime)) {
throw new IllegalArgumentException("End time must be after start time");
}
}
}
IllegalArgumentException for required fields — invalid value objects must never existdomain/events/)package {basepackage}.{boundedcontext}.{subdomain}.domain.events;
/**
* "A Domain Event is a record of some business-significant occurrence in a Bounded Context."
* Vaughn Vernon, Domain-Driven Design Distilled, 2016
*/
public record SessionCreatedEvent(String sessionId, String title) {
}
domain/services/)package {basepackage}.{boundedcontext}.{subdomain}.domain.services;
/**
* "Commands (also known as modifiers) are operations that affect some change to the systems."
* Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, 2003.
*/
public record CreateSessionCommand(String title, String description, TimeSlot timeSlot) {
}
domain/services/)package {basepackage}.{boundedcontext}.{subdomain}.domain.services;
public record SessionCreationResult(Session session, SessionCreatedEvent sessionCreatedEvent) {
}
domain/aggregates/)package {basepackage}.{boundedcontext}.{subdomain}.domain.aggregates;
/**
* "An AGGREGATE is a cluster of associated objects that we treat as a unit
* for the purpose of data changes."
* Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, 2003
*/
public class Session {
String title;
String description;
TimeSlot timeSlot;
protected Session(String title, String description, TimeSlot timeSlot) {
this.title = title;
this.description = description;
this.timeSlot = timeSlot;
}
public static SessionCreationResult createSession(String title, String description, TimeSlot timeSlot) {
Session session = new Session(title, description, timeSlot);
SessionCreatedEvent event = new SessionCreatedEvent(session.getTitle(), session.getTitle());
return new SessionCreationResult(session, event);
}
// Getters (public)
public String getTitle() { return title; }
public String getDescription() { return description; }
public TimeSlot getTimeSlot() { return timeSlot; }
}
Key rules:
protected — external code uses the static factory methodinfrastructure/)package {basepackage}.{boundedcontext}.{subdomain}.infrastructure;
/**
* DTO (Data Transfer Object). DTOs are not specifically a DDD concept.
*/
public record SessionDTO(String title, String description) {
}
infrastructure/ because it serves external adaptersinfrastructure/)package {basepackage}.{boundedcontext}.{subdomain}.infrastructure;
import org.eclipse.microprofile.reactive.messaging.Channel;
import org.eclipse.microprofile.reactive.messaging.Emitter;
import jakarta.enterprise.context.ApplicationScoped;
/**
* "The application has a semantically sound interaction with the adapters on all sides of it,
* without actually knowing the nature of the things on the other side of the adapters."
* Alistair Cockburn, Hexagonal Architecture, 2005.
*/
@ApplicationScoped
public class SessionEventPublisher {
@Channel("sessions")
public Emitter<SessionCreatedEvent> sessionsTopic;
public void publish(SessionCreatedEvent event) {
sessionsTopic.send(event);
}
}
sessions)domain/services/)package {basepackage}.{boundedcontext}.{subdomain}.domain.services;
import io.quarkus.narayana.jta.QuarkusTransaction;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
/**
* "The application and domain layers call on the SERVICES provided by the infrastructure layer."
* Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, 2003.
*/
@ApplicationScoped
public class SessionService {
@Inject
SessionRepository sessionRepository;
@Inject
SessionEventPublisher sessionEventPublisher;
public SessionDTO createSession(CreateSessionCommand command) {
// Domain logic — aggregate factory creates the aggregate and event
SessionCreationResult result = Session.createSession(
command.title(),
command.description(),
command.timeSlot()
);
// Persist in a separate transaction
QuarkusTransaction.requiringNew().run(() -> {
sessionRepository.persist(result.session());
});
// Publish event AFTER persistence succeeds
sessionEventPublisher.publish(result.sessionCreatedEvent());
return new SessionDTO(result.session().getTitle(), result.session().getDescription());
}
}
Critical ordering:
QuarkusTransaction.requiringNew() — isolated transactionUse @Inject field injection. Use @ApplicationScoped.
persistence/)Entity for value objects:
package {basepackage}.{boundedcontext}.{subdomain}.persistence;
import jakarta.persistence.*;
@Entity
public class TimeSlotEntity {
@Id @GeneratedValue
private Long id;
private LocalDateTime startTime;
private LocalDateTime endTime;
protected TimeSlotEntity() {}
protected TimeSlotEntity(LocalDateTime startTime, LocalDateTime endTime) {
this.startTime = startTime;
this.endTime = endTime;
}
// Package-private getters and setters
}
Aggregate entity:
package {basepackage}.{boundedcontext}.{subdomain}.persistence;
import jakarta.persistence.*;
/**
* "An Entity models an individual thing. Each Entity has a unique identity."
* Vaughn Vernon, Domain-Driven Design Distilled, 2016
*/
@Entity
public class SessionEntity {
@Id @GeneratedValue
private Long id;
private String title;
private String description;
@OneToOne(cascade = CascadeType.ALL)
TimeSlotEntity timeSlot;
protected SessionEntity() {}
SessionEntity(String title, String description, TimeSlotEntity timeSlot) {
this.title = title;
this.description = description;
this.timeSlot = timeSlot;
}
// Package-private or protected getters
}
Repository with aggregate-to-entity mapping:
package {basepackage}.{boundedcontext}.{subdomain}.persistence;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
/**
* "A REPOSITORY represents all objects of a certain type as a conceptual set."
* Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software, 2003.
*/
@ApplicationScoped
public class SessionRepository implements PanacheRepository<SessionEntity> {
public void persist(Session aggregate) {
SessionEntity entity = fromAggregate(aggregate);
persist(entity);
}
private SessionEntity fromAggregate(Session session) {
TimeSlotEntity timeSlotEntity = new TimeSlotEntity(
session.getTimeSlot().startTime(),
session.getTimeSlot().endTime()
);
return new SessionEntity(session.getTitle(), session.getDescription(), timeSlotEntity);
}
}
Key rules:
persistence/protected@OneToOne(cascade = CascadeType.ALL)infrastructure/)package {basepackage}.{boundedcontext}.{subdomain}.infrastructure;
import io.quarkus.logging.Log;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.net.URI;
/**
* "The application is blissfully ignorant of the nature of the input device."
* Alistair Cockburn, Hexagonal Architecture, 2005.
*/
@Path("/{subdomain}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class SessionEndpoint {
@Inject
SessionService sessionService;
@POST
public Response createSession(CreateSessionCommand command) {
Log.debugf("Creating session %s", command);
SessionDTO dto = sessionService.createSession(command);
Log.debugf("Created session %s", dto);
return Response.created(URI.create("/" + dto.title())).entity(dto).build();
}
}
{Noun}Endpoint — not Controller or Resource@Path, @Consumes, @Produces at class level201 Created with location URI and DTO bodyLog.debugf()Add to application.properties:
mp.messaging.outgoing.{subdomain}.connector=smallrye-kafka
mp.messaging.outgoing.{subdomain}.topic={subdomain}
These are inviolable:
domain/ → no framework imports (except CDI annotations for service wiring)infrastructure/ → depends on domain/persistence/ → depends on domain/domain/services/ and orchestrates domain logic, persistence, and event publishingWhen asked to generate tests, follow these patterns:
Test method naming: descriptive snake_case sentences.
@Test
void creating_a_session_returns_result_with_event() { }
Domain unit tests: Pure JUnit 5 — no Quarkus container.
class SessionTest {
@Test
void creating_a_session_returns_result_with_event() {
TimeSlot timeSlot = new TimeSlot(LocalDateTime.now(), LocalDateTime.now().plusHours(1));
SessionCreationResult result = Session.createSession("DDD Talk", "About DDD", timeSlot);
assertNotNull(result.session());
assertEquals("DDD Talk", result.sessionCreatedEvent().title());
}
}
Integration tests: @QuarkusTest with Dev Services.
REST API tests: REST Assured with @QuarkusTest.
tools
Use this skill whenever a user wants to create, scaffold, bootstrap, initialize, generate, or set up a new Quarkus application or microservice. Also trigger when a user asks how to add extensions to an existing Quarkus project, upgrade a Quarkus version, or maintain a Quarkus app's dependencies. This skill ensures all projects are generated using the latest official Quarkus platform version, proper tooling (CLI preferred, Maven plugin as fallback), and official platform or Quarkiverse extensions only. Trigger even for casual phrasing like "spin up a Quarkus app", "start a new Quarkus service", "create a Quarkus REST API", or "add a Quarkus extension".
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.
development
Run, watch, debug, and extend OpenClaw QA testing with qa-lab and qa-channel. Use when Codex needs to execute the repo-backed QA suite, inspect live QA artifacts, debug failing scenarios, add new QA scenarios, or explain the OpenClaw QA workflow. Prefer the live OpenAI lane with regular openai/gpt-5.4 in fast mode; do not use gpt-5.4-pro or gpt-5.4-mini unless the user explicitly overrides that policy.