| name | java-patterns |
| description | Modern Java 17+ patterns — records, sealed classes, Stream API, CompletableFuture, Spring Boot, JPA. Activate when writing or reviewing Java. |
| origin | FlowDeck |
Java Patterns Skill
Modern Java for production applications. Focuses on Java 17+ features and Spring Boot conventions.
When to Activate
Activate when:
- Writing new Java services or libraries
- Reviewing Java code for correctness and modern idiom
- Designing API layers, service classes, or data access
- Troubleshooting Spring Boot configuration or JPA queries
- Setting up testing infrastructure
Modern Java Features (17+)
Records — Immutable Data Carriers
Records eliminate boilerplate for data classes. Use them for DTOs, value objects, and command/query objects.
public record Point(double x, double y) {}
public record EmailAddress(String value) {
public EmailAddress {
Objects.requireNonNull(value);
if (!value.contains("@")) {
throw new IllegalArgumentException("invalid email: " + value);
}
value = value.toLowerCase();
}
}
public record PageRequest(int page, int size) implements Serializable {
public PageRequest {
if (page < 0) throw new IllegalArgumentException("page must be >= 0");
if (size < 1 || size > 100) throw new IllegalArgumentException("size must be 1-100");
}
public int offset() { return page * size; }
}
Sealed Classes — Closed Hierarchies
Use sealed classes to model a fixed set of variants. The compiler enforces exhaustive pattern matching.
public sealed interface PaymentResult
permits PaymentResult.Success, PaymentResult.Declined, PaymentResult.Error {
record Success(String transactionId, BigDecimal amount) implements PaymentResult {}
record Declined(String reason) implements PaymentResult {}
record Error(Throwable cause) implements PaymentResult {}
}
String message = switch (result) {
case PaymentResult.Success s -> "Charged " + s.amount();
case PaymentResult.Declined d -> "Declined: " + d.reason();
case PaymentResult.Error e -> "Error: " + e.cause().getMessage();
};
Pattern Matching instanceof
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
if (obj instanceof String s) {
System.out.println(s.length());
}
if (obj instanceof String s && s.length() > 5) {
System.out.println("long string: " + s);
}
Text Blocks
String json = """
{
"name": "Alice",
"age": 30
}
""";
String query = """
SELECT u.id, u.email
FROM users u
WHERE u.active = true
AND u.created_at > :since
""";
Optional — Use at API Boundaries
Optional communicates "might be absent" in a return type. Never use it as a field type or parameter.
public Optional<User> findByEmail(String email) {
return userRepository.findByEmail(email);
}
String displayName = findByEmail(email)
.map(User::displayName)
.orElse("Anonymous");
User user = findByEmail(email)
.orElseThrow(() -> new UserNotFoundException(email));
findByEmail(email).ifPresent(user -> auditLog.record(user.id()));
Optional<User> opt = findByEmail(email);
if (opt != null && opt.isPresent()) { ... }
class Service {
private Optional<Cache> cache;
}
Stream API
Core Operations
List<String> activeEmails = users.stream()
.filter(User::isActive)
.sorted(Comparator.comparing(User::lastName))
.map(User::email)
.toList();
Map<Department, List<Employee>> byDept = employees.stream()
.collect(Collectors.groupingBy(Employee::department));
Map<Boolean, List<User>> partitioned = users.stream()
.collect(Collectors.partitioningBy(User::isAdmin));
BigDecimal total = cart.items().stream()
.map(Item::price)
.reduce(BigDecimal.ZERO, BigDecimal::add);
flatMap — Flattening Nested Collections
List<Item> allItems = users.stream()
.flatMap(u -> u.orders().stream())
.flatMap(o -> o.items().stream())
.toList();
Parallel Streams — When NOT to Use
long count = IntStream.range(0, 10_000_000)
.parallel()
.filter(n -> isPrime(n))
.count();
List<Integer> result = new ArrayList<>();
IntStream.range(0, 1000).parallel()
.forEach(result::add);
Stream.of(urls).parallel()
.map(this::fetch)
.toList();
CompletableFuture — Async Composition
CompletableFuture<String> result = CompletableFuture
.supplyAsync(() -> fetchUser(userId))
.thenApply(User::email)
.thenApplyAsync(this::sendWelcome, executor);
CompletableFuture<Order> order = CompletableFuture
.supplyAsync(() -> findUser(id))
.thenCompose(user -> placeOrder(user, items));
CompletableFuture<User> withFallback = fetchUser(id)
.exceptionally(ex -> {
log.warn("fetch failed, using guest", ex);
return User.guest();
});
CompletableFuture<Void> all = CompletableFuture.allOf(
sendEmail(user), updateCache(user), auditLog(user)
);
all.join();
CompletableFuture<User> withTimeout = fetchUser(id)
.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(ex -> User.guest());
Spring Boot Patterns
Dependency Injection — Constructor Injection Only
@Service
public class OrderService {
private final OrderRepository orders;
private final PaymentGateway payments;
private final EventPublisher events;
public OrderService(
OrderRepository orders,
PaymentGateway payments,
EventPublisher events
) {
this.orders = orders;
this.payments = payments;
this.events = events;
}
}
@Service
public class OrderService {
@Autowired private OrderRepository orders;
}
@ConfigurationProperties — Typed Config
@ConfigurationProperties(prefix = "payment")
public record PaymentConfig(
String apiKey,
URI baseUrl,
Duration timeout,
int maxRetries
) {}
Layered Stereotypes
@Repository
@Service
@Component
@RestController
JPA and Hibernate
N+1 Problem
List<Order> orders = orderRepo.findAll();
orders.forEach(o -> System.out.println(o.getItems().size()));
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.userId = :userId")
List<Order> findWithItems(@Param("userId") Long userId);
@EntityGraph(attributePaths = {"items", "items.product"})
List<Order> findByUserId(Long userId);
Fetch Strategy
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
Transaction Boundaries
@Service
public class TransferService {
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
Account from = accounts.findById(fromId).orElseThrow();
Account to = accounts.findById(toId).orElseThrow();
from.debit(amount);
to.credit(amount);
}
}
Testing
JUnit 5 with Mockito
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock OrderRepository orders;
@Mock PaymentGateway payments;
@InjectMocks OrderService service;
@Test
void placeOrder_chargesPaymentAndSavesOrder() {
var user = aUser().build();
var items = List.of(anItem().withPrice(BigDecimal.TEN).build());
when(payments.charge(any(), any())).thenReturn(chargeSuccess());
var result = service.placeOrder(user, items);
assertThat(result.status()).isEqualTo(OrderStatus.CONFIRMED);
verify(orders).save(argThat(o -> o.total().equals(BigDecimal.TEN)));
}
}
Spring Boot Test Slices
@ExtendWith(MockitoExtension.class)
class UserServiceTest { ... }
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired MockMvc mvc;
@MockBean UserService service;
@Test
void getUser_returns200() throws Exception {
when(service.findById(1L)).thenReturn(Optional.of(aUser().build()));
mvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("alice@example.com"));
}
}
@SpringBootTest(webEnvironment = RANDOM_PORT)
@Transactional
class OrderIntegrationTest { ... }
Build Configuration
Maven
<properties>
<java.version>21</java.version>
<spring-boot.version>3.3.0</spring-boot.version>
</properties>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<release>${java.version}</release>
<compilerArgs><arg>-parameters</arg></compilerArgs>
</configuration>
</plugin>
Gradle (Kotlin DSL)
java {
toolchain { languageVersion = JavaLanguageVersion.of(21) }
}
tasks.withType<JavaCompile> {
options.compilerArgs.add("-parameters")
}
Common Pitfalls
Checked Exceptions in New Code
public List<User> loadUsers() throws IOException, ParseException { ... }
public List<User> loadUsers() {
try {
return parser.parse(Files.readString(path));
} catch (IOException | ParseException e) {
throw new UserLoadException("failed to load users from " + path, e);
}
}
String Concatenation in Loops
String result = "";
for (String s : list) {
result += s + ", ";
}
var sb = new StringBuilder();
for (String s : list) {
sb.append(s).append(", ");
}
String result = sb.toString();
String result = String.join(", ", list);
Autoboxing Overhead
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i;
}
long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i;
}
long sum = LongStream.range(0, 1_000_000).sum();