springboot-patterns

Spring Boot Best Practices

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "springboot-patterns" with this command: npx skills add cliangdev/specflux/cliangdev-specflux-springboot-patterns

Spring Boot Best Practices

Project Structure (Domain-Driven Design)

src/main/java/com/example/ ├── {domain}/ # One package per domain │ ├── domain/ # Domain layer - entities and repository interfaces │ │ ├── {Entity}.java # JPA entity │ │ └── {Entity}Repository.java # Spring Data JPA repository interface │ ├── application/ # Application layer - business logic │ │ └── {Entity}ApplicationService.java │ └── interfaces/ # Interface layer - REST controllers │ └── rest/ │ ├── {Entity}Controller.java │ └── {Entity}Mapper.java ├── shared/ # Cross-cutting concerns │ ├── application/ │ ├── domain/ │ └── interfaces/rest/ └── api/generated/ # OpenAPI generated code (do not edit)

Transaction Management

Minimize Transaction Scope

Only wrap the actual database operation in a transaction, not the entire method:

// Good - minimal transaction scope public EntityDto updateEntity(String ref, UpdateEntityRequestDto request) { Entity entity = refResolver.resolve(ref);

if (request.getTitle() != null) {
    entity.setTitle(request.getTitle());
}
// ... other field updates

Entity saved = transactionTemplate.execute(status -> entityRepository.save(entity));
return entityMapper.toDto(saved);

}

// Bad - entire method in transaction public EntityDto updateEntity(String ref, UpdateEntityRequestDto request) { return transactionTemplate.execute(status -> { Entity entity = refResolver.resolve(ref); // Read doesn't need transaction // ... field updates don't need transaction Entity saved = entityRepository.save(entity); // Only this needs transaction return entityMapper.toDto(saved); }); }

Use TransactionTemplate, Not @Transactional

Prefer TransactionTemplate over @Transactional annotation for explicit control:

@Service @RequiredArgsConstructor public class EntityApplicationService { private final TransactionTemplate transactionTemplate; private final EntityRepository entityRepository;

public void deleteEntity(String ref) {
    Entity entity = refResolver.resolve(ref);
    transactionTemplate.executeWithoutResult(status -> entityRepository.delete(entity));
}

}

When Full Transaction Is Needed

Keep full transaction scope when operations must be atomic:

// Create needs full transaction for sequence number atomicity public EntityDto createEntity(CreateEntityRequestDto request) { return transactionTemplate.execute(status -> { int sequenceNumber = getNextSequenceNumber(); // Read Entity entity = new Entity(..., sequenceNumber, ...); // Must be atomic return entityMapper.toDto(entityRepository.save(entity)); // Write }); }

Lombok Usage

Use @RequiredArgsConstructor for Dependency Injection

Never write constructors manually for Spring beans:

// Good @Service @RequiredArgsConstructor public class EntityApplicationService { private final EntityRepository entityRepository; private final RefResolver refResolver; private final EntityMapper entityMapper; }

// Bad @Service public class EntityApplicationService { private final EntityRepository entityRepository;

public EntityApplicationService(EntityRepository entityRepository) {
    this.entityRepository = entityRepository;
}

}

Standard Lombok Annotations for Entities

@Entity @Table(name = "entities") @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA requires no-arg constructor public class Entity { // fields...

// Required-args constructor for creating new instances
public Entity(String publicId, int sequenceNumber, ...) {
    // initialization
}

}

OpenAPI Generated Code

DTO Suffix Convention

All generated model classes have Dto suffix to distinguish from domain entities:

  • Domain: Entity , Project , Task

  • DTOs: EntityDto , ProjectDto , TaskDto , CreateEntityRequestDto

Never Import with Wildcards

Always use explicit imports:

// Good import com.example.api.generated.model.EntityDto; import com.example.api.generated.model.CreateEntityRequestDto;

// Bad import com.example.api.generated.model.*;

Controller Implementation

Controllers implement generated API interfaces:

@RestController @RequiredArgsConstructor public class EntityController implements EntitiesApi { private final EntityApplicationService entityApplicationService;

@Override
public ResponseEntity<EntityDto> createEntity(CreateEntityRequestDto request) {
    EntityDto created = entityApplicationService.createEntity(request);
    return ResponseEntity.status(HttpStatus.CREATED).body(created);
}

}

Mapper Pattern

Mappers convert between domain entities and DTOs:

@Component public class EntityMapper {

public EntityDto toDto(Entity domain) {
    EntityDto dto = new EntityDto();
    dto.setPublicId(domain.getPublicId());
    dto.setTitle(domain.getTitle());
    dto.setStatus(toApiStatus(domain.getStatus()));
    // ... map all fields
    return dto;
}

public EntityStatus toDomainStatus(EntityStatusDto apiStatus) {
    return switch (apiStatus) {
        case ACTIVE -> EntityStatus.ACTIVE;
        case INACTIVE -> EntityStatus.INACTIVE;
        case ARCHIVED -> EntityStatus.ARCHIVED;
    };
}

private EntityStatusDto toApiStatus(EntityStatus domainStatus) {
    return switch (domainStatus) {
        case ACTIVE -> EntityStatusDto.ACTIVE;
        case INACTIVE -> EntityStatusDto.INACTIVE;
        case ARCHIVED -> EntityStatusDto.ARCHIVED;
    };
}

}

Reference Resolution Pattern

Use RefResolver for looking up entities by publicId or displayKey:

@Component @RequiredArgsConstructor public class RefResolver { private final EntityRepository entityRepository;

public Entity resolve(String ref) {
    return entityRepository.findByPublicId(ref)
        .or(() -> entityRepository.findByKey(ref))
        .orElseThrow(() -> new EntityNotFoundException("Entity", ref));
}

// For optional references (can be null or empty string to clear)
public Entity resolveOptional(String ref) {
    if (ref == null || ref.isBlank()) {
        return null;
    }
    return resolve(ref);
}

}

Exception Handling

Custom Exceptions

public class EntityNotFoundException extends RuntimeException { public EntityNotFoundException(String entityType, String reference) { super(entityType + " not found: " + reference); } }

public class ResourceConflictException extends RuntimeException { public ResourceConflictException(String message) { super(message); } }

Global Exception Handler

@RestControllerAdvice public class GlobalExceptionHandler {

@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponseDto> handleNotFound(EntityNotFoundException ex) {
    ErrorResponseDto error = new ErrorResponseDto();
    error.setCode("NOT_FOUND");
    error.setMessage(ex.getMessage());
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponseDto> handleValidation(MethodArgumentNotValidException ex) {
    ErrorResponseDto error = new ErrorResponseDto();
    error.setCode("VALIDATION_ERROR");
    error.setMessage("Validation failed");
    error.setDetails(ex.getBindingResult().getFieldErrors().stream()
        .map(fe -> {
            FieldErrorDto fieldError = new FieldErrorDto();
            fieldError.setField(fe.getField());
            fieldError.setMessage(fe.getDefaultMessage());
            return fieldError;
        })
        .toList());
    return ResponseEntity.badRequest().body(error);
}

}

Testing Patterns

Integration Tests with Schema Isolation

@AutoConfigureMockMvc @Transactional class EntityControllerTest extends AbstractIntegrationTest {

private static final String SCHEMA_NAME = "entity_controller_test";

@DynamicPropertySource
static void configureSchema(DynamicPropertyRegistry registry) {
    AbstractIntegrationTest.configureSchema(registry, SCHEMA_NAME);
}

@Autowired private MockMvc mockMvc;
@MockitoBean private CurrentUserService currentUserService;

@BeforeEach
void setUp() {
    testUser = userRepository.save(new User(...));
    when(currentUserService.getCurrentUser()).thenReturn(testUser);
}

@Test
@WithMockUser(username = "user")
void createEntity_shouldReturnCreatedEntity() throws Exception {
    CreateEntityRequestDto request = new CreateEntityRequestDto();
    request.setTitle("Test Entity");

    mockMvc.perform(post("/entities")
            .contentType(MediaType.APPLICATION_JSON)
            .content(objectMapper.writeValueAsString(request)))
        .andExpect(status().isCreated())
        .andExpect(jsonPath("$.title").value("Test Entity"));
}

}

Code Style

Checkstyle Rules

  • No star imports (import x.y.* )

  • No unused imports

  • No redundant imports

Spotless Formatting

Run mvn spotless:apply before committing.

Switch Expressions

Use modern switch expressions:

// Good private EntityStatusDto toApiStatus(EntityStatus status) { return switch (status) { case ACTIVE -> EntityStatusDto.ACTIVE; case INACTIVE -> EntityStatusDto.INACTIVE; case ARCHIVED -> EntityStatusDto.ARCHIVED; }; }

// Bad private EntityStatusDto toApiStatus(EntityStatus status) { switch (status) { case ACTIVE: return EntityStatusDto.ACTIVE; case INACTIVE: return EntityStatusDto.INACTIVE; case ARCHIVED: return EntityStatusDto.ARCHIVED; default: throw new IllegalArgumentException(); } }

Build Commands

Generate OpenAPI code

mvn generate-sources

Format code

mvn spotless:apply

Check style

mvn checkstyle:check

Run tests

mvn test

Full build

mvn clean verify

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

ui-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

typescript-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review