- Update
pom.xml- Change
<java.version>17</java.version>to<java.version>21</java.version>. - Ensure you are using Spring Boot 3.2.x or higher (which supports Java 21).
- Change
-
Record Patterns
- Refactor
instanceofchecks inLegacyEmployeeService.java(or modern service if applicable). - Goal: Deconstruct records directly in the
ifstatement. - Example:
// Before if (employee instanceof FullTimeEmployee ft) { return ft.annualSalary(); } // After if (employee instanceof FullTimeEmployee(var id, var name, var email, var salary)) { return salary; }
- Refactor
-
Sequenced Collections
- Status: Reviewed. No immediate use cases found (no direct
get(0)orget(size-1)calls), but available for future use.
- Status: Reviewed. No immediate use cases found (no direct
-
Virtual Threads (The "Killer Feature" of Java 21)
- Enable virtual threads in
src/main/resources/application.properties:spring.threads.virtual.enabled=true - This delegates thread management to the JVM rather than the OS, massively improving throughput for I/O-bound apps.
- Enable virtual threads in
- Run
mvn clean installto ensure the build passes. - Run the app and verify endpoints still work.
-
Refactor
if-elseto Switch with Pattern Matching- In
EmployeeService.java, replace theif-elsechains usinginstanceofwith a modernswitchexpression. - Why: It's more idiomatic, concise, and the compiler can check for exhaustiveness (if the interface is sealed).
- Target:
calculatePayandmapToEntitymethods. - Example:
switch (employee) { case FullTimeEmployee(var id, var name, var email, var salary) -> entity.setSalary(salary); case Contractor(var id, var name, var email, var rate, var hours) -> { entity.setSalary(rate); entity.setHoursWorked(hours); } }
- In
-
Enable Generational ZGC
- If this application is intended for high performance/low latency, consider enabling the new Generational ZGC.
- Action: Add
-XX:+UseZGC -XX:+ZGenerationalto your JVM run arguments.
-
Explore Unnamed Patterns (Preview Feature)
- Note: This requires enabling preview features (
--enable-preview). - You have unused variables in your patterns (e.g.,
id,name,emailincalculatePay). - You can replace them with
_to clearly indicate they are unused. - Example:
case FullTimeEmployee(_, _, _, var salary)
- Note: This requires enabling preview features (
-
Use BigDecimal for Monetary Values
- Observation: Currently
Doubleis used forsalaryandhourlyRate. - Recommendation: Use
java.math.BigDecimalfor financial calculations to avoid floating-point precision errors. - Target:
EmployeeEntity,Employeerecords, andEmployeeService.
- Observation: Currently
-
Add Test Coverage for Pay Calculation
- Observation:
EmployeeControllerTestcovers creation and retrieval, but not thecalculatePaylogic. - Recommendation: Add a test case for
GET /employees/{id}/payto verify the newBigDecimallogic.
- Observation:
-
Consider String Templates (Preview)
- Note: Since preview features are enabled, you could use String Templates for cleaner string concatenation.
- Example:
STR."Employee not found with id: \{id}"instead of"Employee not found with id: " + id.
These tasks are focused on demonstrating Java 21 features in small, targeted places in the app, rather than rewriting everything.
-
Pattern Matching for
switchwith Record Patterns- Goal: Use record patterns directly inside a
switchover the sealedEmployeehierarchy. - Where: Modern
EmployeeService(e.g.,calculatePayor an internal mapping method). - Example:
// Example: calculate pay using switch + record patterns BigDecimal calculatePay(Employee employee) { return switch (employee) { case FullTimeEmployee(var id, var name, var email, var salary) -> salary; case Contractor(_, _, _, var hourlyRate, var hoursWorked) -> hourlyRate.multiply(hoursWorked); }; }
- Goal: Use record patterns directly inside a
-
Structured Concurrency with
StructuredTaskScope-
Goal: Demonstrate structured concurrency by running a couple of related tasks in parallel and waiting for both.
-
Where: Create a small method in a service layer, e.g.
EmployeeInsightsService, that fetches:- employee details, and
- calculated pay
concurrently using virtual threads.
-
Example:
EmployeeInsights getInsights(Long employeeId) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { var detailsTask = scope.fork(() -> employeeService.getEmployee(employeeId)); var payTask = scope.fork(() -> employeeService.calculatePay(employeeId)); scope.join(); // Wait for both scope.throwIfFailed(); // Propagate first failure var details = detailsTask.resultNow(); var pay = payTask.resultNow(); return new EmployeeInsights(details, pay); } }
-
-
Scoped Values (Replacing Simple ThreadLocal Use Cases)
-
Goal: Show how to use
ScopedValueas a modern alternative toThreadLocalfor simple contextual data (e.g., correlation ID). -
Where: Create a small utility or filter that sets a correlation ID per request and makes it available in the service layer.
-
Example:
public final class RequestContext { public static final ScopedValue<String> CORRELATION_ID = ScopedValue.newInstance(); } // In a filter or controller wrapper void handleRequest(Runnable handler) { String correlationId = UUID.randomUUID().toString(); ScopedValue.where(RequestContext.CORRELATION_ID, correlationId) .run(handler); } // In a service void logSomething() { String correlationId = RequestContext.CORRELATION_ID.orElse("unknown"); log.info("Processing with correlationId={}", correlationId); }
-
-
Sequenced Collections
-
Goal: Use
SequencedCollection/SequencedSetAPIs where order matters (first/last elements or reverse views). -
Where: Any place where you return a list of employees and might want to:
- add an employee at the beginning or end,
- access the first/last employee,
- iterate in reverse order.
-
Example:
SequencedCollection<Employee> employees = new ArrayList<>(); employees.addLast(fullTimeEmployee); employees.addFirst(contractor); Employee first = employees.getFirst(); Employee last = employees.getLast(); SequencedCollection<Employee> reversed = employees.reversed();
-
-
Text Blocks for JSON/SQL in Tests
- Goal: Improve readability of multi-line strings used in tests (JSON payloads, SQL snippets).
- Where: Test code for controllers/services, or any embedded SQL used for demos.
- Example:
String newEmployeeJson = """ { "type": "FULL_TIME", "name": "Alice", "email": "alice@example.com", "annualSalary": 120000 } """;