// Config Record
>Java + Spring Boot 3
GitHub Copilot instructions for Java 21 and Spring Boot 3 projects with modern testing, security, and architecture defaults.
author:
dotmd Team
license:CC0
published:Feb 23, 2026
// Installation
>Add this file to your project repository:
- GitHub Copilot--path=
.github/copilot-instructions.md
// File Content
copilot-instructions.md
1# Project: Java 21 + Spring Boot 323## Language & Runtime45- Java 21 — use records, sealed interfaces, pattern matching, text blocks, virtual threads6- Spring Boot 3.x (Jakarta EE 10 — all imports `jakarta.*`, never `javax.*`)7- Build: Maven with `spring-boot-starter-parent` or Gradle with Spring Dependency Management plugin89## Java 21 Patterns1011**Records for all DTOs and value objects:**1213```java14public record CreateOrderRequest(15 @NotBlank String customerId,16 @NotEmpty List<OrderLineRequest> lines) {}1718public record OrderResponse(UUID id, String status, Instant createdAt) {}19```2021**Sealed interfaces for domain type hierarchies:**2223```java24public sealed interface PaymentResult {25 record Success(String transactionId, Instant processedAt) implements PaymentResult {}26 record Declined(String reason) implements PaymentResult {}27 record Error(String code, String message) implements PaymentResult {}28}29```3031**Pattern matching in switch — exhaustive, no default needed with sealed types:**3233```java34return switch (result) {35 case PaymentResult.Success s -> ResponseEntity.ok(toResponse(s));36 case PaymentResult.Declined d -> ResponseEntity.unprocessableEntity().body(problem(d.reason()));37 case PaymentResult.Error e -> ResponseEntity.internalServerError().body(problem(e.message()));38};39```4041**Text blocks for queries and multi-line strings:**4243```java44@Query("""45 SELECT o FROM Order o JOIN FETCH o.lines46 WHERE o.customer.id = :customerId AND o.status IN :statuses47 """)48List<Order> findByCustomerAndStatuses(UUID customerId, Set<OrderStatus> statuses);49```5051**Virtual threads — enable in config, no code changes needed:**5253```yaml54spring.threads.virtual.enabled: true55```5657## Package Structure5859```60com.example.ordersvc/61├── OrderServiceApplication.java62├── config/ # @Configuration classes63├── order/ # Feature package64│ ├── Order.java # @Entity65│ ├── OrderStatus.java # enum66│ ├── OrderRepository.java # Spring Data interface67│ ├── OrderService.java # @Service68│ ├── OrderController.java # @RestController69│ └── dto/ # Request/response records70├── customer/ # Another feature package71└── common/72 ├── exception/ # GlobalExceptionHandler, domain exceptions73 └── web/ # Shared web utilities74```7576Organize by **feature**, not by layer. Each feature owns its controller, service, repository, entities, and DTOs.7778## Dependency Injection7980**Constructor injection only. No `@Autowired`. No field injection.**8182```java83@Service84public class OrderService {85 private final OrderRepository orderRepository;86 private final PaymentClient paymentClient;87 private final ApplicationEventPublisher events;8889 public OrderService(OrderRepository orderRepository,90 PaymentClient paymentClient,91 ApplicationEventPublisher events) {92 this.orderRepository = orderRepository;93 this.paymentClient = paymentClient;94 this.events = events;95 }96}97```9899Single constructor — `@Autowired` is not needed.100101## REST Controllers102103```java104@RestController105@RequestMapping("/api/v1/orders")106public class OrderController {107 private final OrderService orderService;108109 public OrderController(OrderService orderService) {110 this.orderService = orderService;111 }112113 @PostMapping114 @ResponseStatus(HttpStatus.CREATED)115 public OrderResponse create(@Valid @RequestBody CreateOrderRequest request) {116 return orderService.create(request);117 }118119 @GetMapping("/{id}")120 public OrderResponse findById(@PathVariable UUID id) {121 return orderService.findById(id);122 }123124 @GetMapping125 public PageResponse<OrderResponse> list(126 @RequestParam(defaultValue = "0") int page,127 @RequestParam(defaultValue = "20") int size) {128 return orderService.list(PageRequest.of(page, size, Sort.by("createdAt").descending()));129 }130}131```132133- Return records directly — Jackson handles them134- `@Valid` on `@RequestBody` for bean validation135- `UUID` for path IDs, not `Long`136137## Service Layer138139```java140@Service141@Transactional(readOnly = true)142public class OrderService {143144 @Transactional145 public OrderResponse create(CreateOrderRequest request) { /* ... */ }146147 public OrderResponse findById(UUID id) {148 return orderRepository.findById(id)149 .map(this::toResponse)150 .orElseThrow(() -> new ResourceNotFoundException("Order", id));151 }152}153```154155- Class-level `@Transactional(readOnly = true)`, override with `@Transactional` on writes156- Never return entities — map to response records157- Never call `Optional.get()` — use `orElseThrow()`, `map()`, or `ifPresent()`158159## Entity Design160161```java162@Entity163@Table(name = "orders")164public class Order {165 @Id166 @GeneratedValue(strategy = GenerationType.UUID)167 private UUID id;168169 @Column(nullable = false)170 private String customerId;171172 @Enumerated(EnumType.STRING)173 @Column(nullable = false)174 private OrderStatus status;175176 @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)177 private List<OrderLine> lines = new ArrayList<>();178179 @Column(nullable = false, updatable = false)180 private Instant createdAt;181182 @PrePersist183 void onCreate() { this.createdAt = Instant.now(); }184185 protected Order() {} // JPA requires no-arg constructor186187 public Order(String customerId) {188 this.customerId = customerId;189 this.status = OrderStatus.DRAFT;190 }191192 public void addLine(OrderLine line) {193 lines.add(line);194 line.setOrder(this);195 }196}197```198199- `GenerationType.UUID` — not AUTO or IDENTITY200- `EnumType.STRING` — never ORDINAL201- `Instant` for timestamps — never `LocalDateTime` or `java.util.Date`202- Entities are mutable classes; records are for DTOs only203- Encapsulate collections (`addLine()`, not `getLines().add()`)204205## Database Migrations — Flyway206207```208src/main/resources/db/migration/209├── V1__create_orders.sql210├── V2__create_order_lines.sql211└── V3__add_order_status_index.sql212```213214```yaml215spring:216 flyway:217 enabled: true218 locations: classpath:db/migration219 jpa:220 hibernate:221 ddl-auto: validate # Never 'update' or 'create' in production222```223224## Error Handling — RFC 9457 Problem Details225226```java227@RestControllerAdvice228public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {229230 @ExceptionHandler(ResourceNotFoundException.class)231 public ProblemDetail handleNotFound(ResourceNotFoundException ex) {232 var problem = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());233 problem.setTitle("Resource Not Found");234 problem.setProperty("resourceType", ex.getResourceType());235 return problem;236 }237238 @ExceptionHandler(BusinessRuleException.class)239 public ProblemDetail handleBusinessRule(BusinessRuleException ex) {240 var problem = ProblemDetail.forStatusAndDetail(HttpStatus.UNPROCESSABLE_ENTITY, ex.getMessage());241 problem.setTitle("Business Rule Violation");242 problem.setProperty("code", ex.getCode());243 return problem;244 }245}246```247248Enable built-in problem details: `spring.mvc.problemdetails.enabled: true`249250## Spring Security 6251252```java253@Configuration254@EnableWebSecurity255public class SecurityConfig {256257 @Bean258 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {259 return http260 .csrf(csrf -> csrf.ignoringRequestMatchers("/api/**"))261 .authorizeHttpRequests(auth -> auth262 .requestMatchers("/api/v1/public/**").permitAll()263 .requestMatchers("/actuator/health").permitAll()264 .requestMatchers("/api/v1/admin/**").hasRole("ADMIN")265 .anyRequest().authenticated())266 .oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))267 .sessionManagement(session -> session268 .sessionCreationPolicy(SessionCreationPolicy.STATELESS))269 .build();270 }271}272```273274- Lambda DSL only — `.and()` chain is removed in Security 6275- `@EnableMethodSecurity` for `@PreAuthorize` on methods276- Never `.permitAll()` on `anyRequest()`277278## Testing279280### Controller Tests — `@WebMvcTest` (fast, no server)281282```java283@WebMvcTest(OrderController.class)284class OrderControllerTest {285 @Autowired private MockMvc mockMvc;286 @Autowired private ObjectMapper objectMapper;287 @MockitoBean private OrderService orderService;288289 @Test290 void shouldCreateOrder() throws Exception {291 var response = new OrderResponse(UUID.randomUUID(), "DRAFT", Instant.now());292 when(orderService.create(any())).thenReturn(response);293294 mockMvc.perform(post("/api/v1/orders")295 .contentType(APPLICATION_JSON)296 .content(objectMapper.writeValueAsString(297 new CreateOrderRequest("cust-1", List.of(new OrderLineRequest("SKU-001", 2))))))298 .andExpect(status().isCreated())299 .andExpect(jsonPath("$.status").value("DRAFT"));300 }301}302```303304- `@MockitoBean` replaces `@MockBean` (Spring Boot 3.4+)305- AssertJ for assertions, not Hamcrest306307### Integration Tests — `@SpringBootTest` + Testcontainers308309```java310@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)311@Testcontainers312class OrderIntegrationTest {313 @Container @ServiceConnection314 static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");315316 @Autowired private TestRestTemplate restTemplate;317318 @Test319 void shouldCreateAndRetrieveOrder() {320 var request = new CreateOrderRequest("cust-1", List.of(new OrderLineRequest("SKU-001", 2)));321 var created = restTemplate.postForEntity("/api/v1/orders", request, OrderResponse.class);322 assertThat(created.getStatusCode()).isEqualTo(HttpStatus.CREATED);323 }324}325```326327- `@ServiceConnection` auto-configures datasource — no manual properties needed328- `@DataJpaTest` + `@AutoConfigureTestDatabase(replace = NONE)` for repository-only tests329330## Configuration331332Bind config to records with `@ConfigurationProperties`:333334```java335@ConfigurationProperties(prefix = "app.orders")336public record OrderProperties(337 int maxLinesPerOrder,338 Duration processingTimeout,339 RetryProperties retry340) {341 public record RetryProperties(int maxAttempts, Duration backoff) {}342}343```344345Profile-specific files: `application.yml`, `application-local.yml`, `application-prod.yml`.346347## HTTP Clients348349Use `RestClient` (Spring 6.1+), not `RestTemplate`. For declarative clients, use `@HttpExchange`:350351```java352@HttpExchange("/api/v1/payments")353public interface PaymentClient {354 @PostExchange355 PaymentResult process(@RequestBody PaymentRequest request);356357 @GetExchange("/{id}")358 PaymentStatus status(@PathVariable String id);359}360```361362## Observability363364```yaml365management:366 endpoints.web.exposure.include: health,info,prometheus,metrics367 endpoint.health.show-details: when_authorized368```369370Use Micrometer `@Observed` for custom metrics. Structured logging:371372```java373log.info("Order created [orderId={}, customerId={}]", order.getId(), order.getCustomerId());374```375376## Commands377378```bash379./mvnw spring-boot:run -Dspring-boot.run.profiles=local # Run380./mvnw test # Unit tests381./mvnw verify # Unit + integration382./mvnw clean package -DskipTests # Build JAR383./mvnw spring-boot:build-image # OCI image384./mvnw flyway:migrate # Run migrations385```386387## Style Rules388389- `var` for local variables when type is obvious from right-hand side390- `List.of()`, `Map.of()`, `Set.of()` — immutable by default391- `java.time` exclusively — no `java.util.Date` or `java.sql.Timestamp`392- Return `Optional<T>` from lookups — never return null393- Unchecked domain exceptions only — no checked exceptions in service interfaces394- `final` on all injected fields395