Claude Code Plugins

Community-maintained marketplace

Feedback

backend-development

@zabiny/klabis
2
0

Provides development guidance for implementing backend features using Domain-Driven Design, hexagonal architecture, event-driven patterns, and HATEOAS conventions. Use when implementing new domain features, REST APIs, or integrating cross-domain functionality in the backend.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name backend-development
description Provides development guidance for implementing backend features using Domain-Driven Design, hexagonal architecture, event-driven patterns, and HATEOAS conventions. Use when implementing new domain features, REST APIs, or integrating cross-domain functionality in the backend.

Backend DDD Development Patterns Guide

Architecture Overview

This project follows Domain-Driven Design (DDD) principles with hexagonal architecture:

Domain Layer
├── Aggregate Roots (@AggregateRoot)
├── Entities (@Entity)
├── Value Objects (Records)
├── Commands
└── Repository Interfaces (Secondary Ports)

Infrastructure Layer
├── REST API Controllers (@ApiController)
├── Application Services (@Service)
├── Repository Implementations (Secondary Adapters)
├── Event Listeners (@EventListener)
└── Jackson Converters (@JacksonComponent)

Reference Implementation

The club.klabis.calendar package demonstrates these patterns:

  • Domain: Calendar aggregate with CalendarItem entities
  • Commands: CreateCalendarItemCommand for user intents
  • Events: EventListeners for cross-domain integration
  • API: CalendarApiController with HATEOAS support

Key Design Patterns

1. DDD Annotations (jMolecules)

Mark architectural roles explicitly:

@AggregateRoot      // Marks the aggregate root class
@Entity             // Marks entities within aggregates
@Identity           // Marks identity fields
@Service            // Marks application/domain services
@Repository         // Marks repository interfaces

Usage: Apply at class level to clarify domain semantics.

2. Aggregate Root Pattern

An aggregate is a cluster of domain objects treated as a single unit:

Characteristics:

  • One aggregate root (public entry point)
  • Contains entities and value objects
  • Enforces business rules and invariants
  • Has identity field marked with @Identity
  • Manages its internal consistency

Example structure:

@AggregateRoot
public class MyAggregate extends AbstractAggregateRoot<MyAggregate> {
    @Identity
    private AggregateId id;
    private Set<InternalEntity> children = new HashSet<>();

    // Business rule enforcement
    public void doSomething(Command cmd) {
        if (!isValidFor(cmd)) {
            throw new IllegalArgumentException("Cannot execute");
        }
        // Modify state
        this.children.add(new InternalEntity(...));
    }

    public Set<InternalEntity> getChildren() {
        return Collections.unmodifiableSet(children);
    }
}

3. Entity Design

Entities have identity and mutable state:

Characteristics:

  • Identity field (typically nested record with value wrapper)
  • Marked with @Entity within aggregate
  • Can be modified through methods
  • Equality based on identity, not state

Example:

@Entity
public class ItemEntity {
    @Identity
    private Id id;
    private String status;
    private LocalDateTime createdAt;

    public record Id(long value) {
        static long MAX_VALUE = 0L;
        static synchronized Id newId() {
            return new Id(MAX_VALUE++);
        }
    }

    public static ItemEntity create() {
        return new ItemEntity(Id.newId(), "PENDING", LocalDateTime.now());
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) return true;
        if (obj == null || obj.getClass() != this.getClass()) return false;
        var that = (ItemEntity) obj;
        return this.id == that.id;  // Identity-based equality
    }
}

4. Value Objects as Records

Use immutable Java records for value objects:

public record DateRange(LocalDate start, LocalDate end) {
    public DateRange {
        // Validation in compact constructor
        Assert.isTrue(!start.isAfter(end), "Start must not be after end");
    }

    public boolean includes(LocalDate date) {
        return !start.isAfter(date) && !end.isBefore(date);
    }
}

Benefits:

  • Immutability enforced by compiler
  • Auto-generated equals/hashCode/toString
  • Compact syntax for simple objects

5. Command Pattern

Commands encapsulate user intents and data:

public record CreateItemCommand(String name, String description, LocalDate dueDate) {
    public CreateItemCommand {
        // Validation in compact constructor
        Assert.hasText(name, "Name required");
    }

    // Factory methods for common cases
    public static CreateItemCommand quickTask(String name) {
        return new CreateItemCommand(name, "", LocalDate.now());
    }
}

Command Handler:

public class MyAggregate extends AbstractAggregateRoot<MyAggregate> {
    public Item handle(CreateItemCommand command) {
        if (!canCreate(command)) {
            throw new IllegalArgumentException("Cannot create item");
        }
        Item item = Item.create(command.name(), command.description());
        this.items.add(item);
        return item;
    }
}

6. Repository Pattern

Repository interfaces define the contract for data access (Secondary Port):

public interface MyRepository extends DataRepository<Item, Item.Id> {
    // Domain-specific queries
    Optional<Item> findByName(String name);
    List<Item> findAllActive();
    Optional<MyAggregate> readAggregate(AggregateId id);
}

Design Principles:

  • Interface in domain package (secondary port)
  • Implementation in infrastructure (secondary adapter)
  • Use domain types, not DTOs
  • Query methods return domain objects
  • readAggregate() loads full aggregate with children

7. Application Service Layer

Services coordinate domain logic with infrastructure:

@Service
@Component
public class MyApplicationService {
    private final MyRepository repository;
    private final EventPublisher eventPublisher;

    public MyApplicationService(MyRepository repository, EventPublisher eventPublisher) {
        this.repository = repository;
        this.eventPublisher = eventPublisher;
    }

    @Transactional
    public Item createItem(CreateItemCommand command) {
        MyAggregate agg = repository.readAggregate(command.aggregateId());
        Item item = agg.handle(command);
        repository.save(agg);
        return item;
    }
}

Responsibilities:

  • Orchestrate domain operations
  • Handle transactions with @Transactional
  • Publish domain events
  • Convert between domain and API layers

8. REST API Controller

Controllers expose domain functionality via HTTP:

@ApiController(openApiTagName = "items", path = "/items")
@ExposesResourceFor(Item.class)
public class ItemApiController {

    private final MyApplicationService service;
    private final ModelAssembler<Item, ItemDto> assembler;

    @Relation(collectionRelation = "items")
    public record ItemDto(String name, String status, Link parentLink) {}

    @GetMapping
    public CollectionModel<EntityModel<ItemDto>> listItems() {
        List<Item> items = service.listAll();
        return assembler.toCollectionModel(items);
    }

    @PostMapping
    public ResponseEntity<Void> createItem(@RequestBody CreateItemCommand cmd) {
        Item item = service.createItem(cmd);
        return ResponseEntity.created(toSelfLink(item).toUri()).build();
    }
}

Patterns:

  • DTOs for API contracts with @Relation annotation
  • Convert domain objects to DTOs
  • Use ModelAssembler for HATEOAS links
  • @ExposesResourceFor declares exposed domain type
  • Separate postprocessors for enriching responses

9. HATEOAS Representation Processors

Enrich API responses with hypermedia links:

@Component
class ItemRepresentationProcessor
    implements RepresentationModelProcessor<EntityModel<ItemDto>> {

    @Override
    public EntityModel<ItemDto> process(EntityModel<ItemDto> model) {
        model.add(linkTo(methodOn(ItemApiController.class)
            .updateItem(null)).withRel("update"));
        return model;
    }
}

@Component
class ItemListRepresentationProcessor
    implements RepresentationModelProcessor<CollectionModel<EntityModel<ItemDto>>> {

    @Override
    public CollectionModel<EntityModel<ItemDto>> process(CollectionModel<...> model) {
        model.add(linkTo(methodOn(ItemApiController.class)
            .createItem(null)).withRel("create"));
        return model;
    }
}

10. Event-Driven Integration

React to domain events from other bounded contexts:

@Component
class DomainEventListeners {
    private final MyRepository repository;

    @EventListener(OtherDomainEventOccurred.class)
    public void onExternalEvent(OtherDomainEventOccurred event) {
        // React to event from another bounded context
        Item item = repository.findById(itemId)
            .orElseGet(() -> Item.createFrom(event));

        // Update or create based on event data
        item.updateFrom(event);
        repository.save(item);
    }
}

Patterns:

  • Use @EventListener with event class
  • Extract aggregate from event: event.getAggregate()
  • Find existing or create new: findById().orElseGet()
  • React asynchronously to cross-domain changes

11. Jackson Custom Serialization

Handle special type serialization:

@JacksonComponent
class ItemIdSerializer extends ValueSerializer<Item.Id>
    implements Converter<Item.Id, String> {

    @Override
    public void serialize(Item.Id value, JsonGenerator gen, SerializationContext ctxt) {
        ctxt.findValueSerializer(Long.class)
            .serialize(value?.value(), gen, ctxt);
    }

    @Override
    public String convert(Item.Id source) {
        return source != null ? Long.toString(source.value()) : null;
    }
}

Used for: Converting domain value objects to/from JSON in API layer.

Development Workflow

Implementing a New Feature

  1. Analyze the requirement

    • Identify aggregate roots
    • Define entities and value objects
    • Determine invariants and business rules
  2. Design domain model

    • Create aggregate root with @AggregateRoot
    • Add entities with @Entity
    • Use records for value objects
    • Implement business rules in aggregate
  3. Create commands

    • Define command records
    • Add validation in compact constructors
    • Create factory methods for common cases
  4. Define repository contract

    • Create interface extending DataRepository<T, ID>
    • Add domain-specific query methods
    • Include aggregate loading method
  5. Implement repository

    • Create adapter class in infrastructure
    • Implement all interface methods
    • Handle queries with business-appropriate logic
  6. Create application service

    • Inject repository interface
    • Create public methods for use cases
    • Implement command handling via aggregate.handle()
    • Add @Transactional for writes
  7. Build REST API

    • Create DTOs with @Relation
    • Implement controller methods
    • Use ModelAssembler for HATEOAS
    • Create representation processors
  8. Add event listeners (if needed)

    • Create listener component
    • React to external domain events
    • Update/create domain objects
    • Save to repository

Using Factory Methods

Create consistent object construction:

// In aggregate
public static MyAggregate create(Id id) {
    return new MyAggregate(id, new HashSet<>(), LocalDateTime.now());
}

// In entity
public static Item task(String name) {
    return new Item(Id.newId(), name, ItemStatus.TODO);
}

// In command
public static CreateItemCommand quickItem(String name) {
    return new CreateItemCommand(name, "", LocalDate.now());
}

Immutable Collections

Always return unmodifiable collections from aggregates:

public Set<Item> getItems() {
    return Collections.unmodifiableSet(items);
}

public List<Item> getOrderedItems() {
    return Collections.unmodifiableList(new ArrayList<>(items));
}

Fluent/Builder API

Enable method chaining:

public Item withStatus(ItemStatus status) {
    this.status = status;
    return this;
}

public Item withDescription(String description) {
    this.description = description;
    return this;
}

// Usage
Item item = Item.create()
    .withStatus(ACTIVE)
    .withDescription("Important task");

DateTime Handling

Convention:

  • Domain: Use ZonedDateTime for temporal data with timezone info
  • Value Objects: Use LocalDate or LocalDateTime for timezone-agnostic data
  • API: Convert to LocalDate in DTOs
  • Conversion: Use shared utility functions
// In domain
private ZonedDateTime createdAt = ZonedDateTime.now();

// In DTO (API)
public record ItemDto(LocalDate createdDate, ...) {}

// Conversion in controller
ItemDto toDto(Item item) {
    return new ItemDto(Globals.toLocalDate(item.getCreatedAt()), ...);
}

Code Style Conventions

Naming

Classes:        PascalCase (MyAggregate, ItemEntity)
Methods:        camelCase (createItem, readAggregate)
Constants:      UPPER_SNAKE_CASE (MAX_ITEMS, DEFAULT_STATUS)
Records:        PascalCase (ItemDto, CreateItemCommand)
Packages:       lowercase.reversed.domain (org.club.klabis.feature.domain)

Method Organization

  1. Annotations
  2. Fields/Properties
  3. Nested Classes/Records
  4. Constructors
  5. Factory/Builder methods (static)
  6. Public behavior methods
  7. Accessor methods (getters)
  8. equals/hashCode/toString

Validation Style

// In compact record constructor - for value objects
public record DateRange(LocalDate start, LocalDate end) {
    public DateRange {
        Assert.isTrue(!start.isAfter(end), "Start must not be after end");
    }
}

// In methods - for business logic errors
public void schedule(LocalDateTime time) {
    if (time.isBefore(LocalDateTime.now())) {
        throw new IllegalArgumentException("Cannot schedule in past");
    }
}

Import Organization

import java.*;          // Java standard library
import java.time.*;     // Java time APIs
import org.springframework.*;  // Framework
import org.jmolecules.*; // DDD annotations
import club.klabis.*;    // Local packages

Common Patterns Reference

Find or Create Pattern

Item item = repository.findById(id)
    .map(existing -> {
        existing.updateFrom(event);
        return existing;
    })
    .orElseGet(() -> Item.createFrom(event));

repository.save(item);

Optional Chaining

Optional<Item> result = repository.findById(id)
    .filter(item -> item.isActive())
    .map(item -> item.withStatus(UPDATED));

Validation in Constructor

protected MyAggregate(Id id, Set<Item> items) {
    Assert.notNull(id, "ID required");
    Assert.notEmpty(items, "At least one item required");
    this.id = id;
    this.items.addAll(items);
}

Transaction Boundary

@Transactional
public Item processCommand(Command cmd) {
    MyAggregate agg = repository.readAggregate(cmd.aggregateId());
    Item result = agg.handle(cmd);
    return repository.save(agg);
}

HATEOAS Link Building

Link selfLink = linkTo(methodOn(ItemController.class)
    .getItem(item.getId())).withSelfRel();

model.add(selfLink);
model.add(linkTo(methodOn(ItemController.class)
    .deleteItem(item.getId())).withRel("delete"));

Testing Patterns

Unit Test - Domain Logic

@Test
void aggregateEnforcesBusinessRule() {
    var agg = MyAggregate.create(id);

    assertThrows(IllegalArgumentException.class, () -> {
        agg.doInvalidOperation();
    });
}

Unit Test - Repository Mock

@Test
void serviceUsesRepository() {
    var repo = mock(MyRepository.class);
    var service = new MyApplicationService(repo);

    when(repo.findById(id)).thenReturn(Optional.of(item));
    var result = service.getItem(id);

    verify(repo).findById(id);
    assertNotNull(result);
}

Integration Test - End-to-End

@Test
void endToEndFeatureWorks() {
    var cmd = CreateItemCommand.quickItem("Test");
    var result = service.createItem(cmd);

    var found = repository.findById(result.getId());
    assertTrue(found.isPresent());
    assertEquals("Test", found.get().getName());
}

FAQ

Q: Should I put business logic in the service or the aggregate? A: Business logic belongs in the aggregate. The service orchestrates and handles transactions.

Q: How do I handle validation errors? A: Use exceptions for user input errors. Use Assert for internal invariants. Catch and convert to HTTP errors in controller.

Q: When should I use an event listener vs a direct service call? A: Use event listeners for cross-domain/bounded context integration. Use direct calls within the same domain.

Q: What's the difference between a Command and a DTO? A: Commands represent user intent with business semantics. DTOs are data transfer objects for serialization.

Q: How do I model optional fields? A: Use @Nullable annotation. Handle nulls in value object methods. Use Optional in query methods.

Q: Should repository queries return Optional or List? A: Single item by identity: Optional<T>. Multiple items or searches: List<T>.

Common Gotchas

  1. Modifying collections from outside: Always return unmodifiable collections
  2. Equality checks: Base entity equality on identity, not state
  3. Lazy loading: Eager load children when reading aggregate
  4. Transaction scope: Ensure related operations are in same transaction
  5. DTO pollution: Don't use DTOs in domain layer
  6. Event ordering: Event listeners may execute in any order - design for this
  7. Null handling: Use @Nullable annotations and check before dereferencing

References

Package Locations:

  • Domain layer: src/main/java/club/klabis/{feature}/
  • Infrastructure: src/main/java/club/klabis/{feature}/infrastructure/
  • Tests: src/test/java/club/klabis/{feature}/

Key Classes (Examples from reference implementation):

  • Aggregate: club.klabis.calendar.Calendar
  • Entity: club.klabis.calendar.CalendarItem
  • Command: club.klabis.calendar.CreateCalendarItemCommand
  • Repository: club.klabis.calendar.CalendarRepository
  • Service: club.klabis.calendar.infrastructure.CalendarService
  • Controller: club.klabis.calendar.infrastructure.CalendarApiController
  • Listener: club.klabis.calendar.infrastructure.EventListeners