Java
Purpose
Guide for Java build tools (Maven/Gradle), module system, modern language features (records, sealed classes, pattern matching), streams, concurrency, testing, and production deployment.
Agent Protocol
Trigger
Keywords: java build, maven, gradle, java module, record, stream, optional, completablefuture, junit, spring boot, jakarta.
Input Context
- Build tool (Maven, Gradle)
- Java version
- Framework (Spring Boot, Quarkus, Micronaut, Jakarta EE)
- Deployment target
Decision Trees
Build Tool Selection
Team preference?
├── Declarative, XML, convention → Maven (stable, predictable, standard)
│ ├── pom.xml, lifecycle phases (compile, test, package)
│ └── Best for: large orgs, strict conventions, established projects
├── Flexible, programmable, fast → Gradle (Kotlin DSL, incremental builds)
│ ├── build.gradle.kts, task-based, build cache
│ └── Best for: Android, multi-module, custom build logic
└── Modern, fast, no-nonsense → Maven + mvnd (Maven Daemon, parallel builds)
Framework Selection
What kind of application?
├── Full-stack web app → Spring Boot (most mature, vast ecosystem, auto-config)
├── Cloud-native / low-resource → Quarkus (GraalVM native, fast startup)
├── Microservices / reactive → Micronaut (compile-time DI, AOT)
├── REST API / lightweight → JAX-RS (Jakarta REST) + Helidon / Dropwizard
└── Batch processing → Spring Batch / Apache Beam
Module Strategy
Project size?
├── Small (<10k LOC) → Single module, package-based organization
├── Medium (10-100k LOC) → Multi-module with clear boundaries (api, core, infra)
├── Large (>100k LOC) → Java Platform Module System (JPMS) with module-info.java
└── Library → Single module, minimize exports, use JPMS for strong encapsulation
Build & Dependency Management
Maven pom.xml
<project>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
</parent>
<properties>
<java.version>21</java.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
Gradle build.gradle.kts
plugins {
java
id("org.springframework.boot") version "3.3.0"
id("io.spring.dependency-management") version "1.1.5"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
compileOnly("org.projectlombok:lombok")
annotationProcessor("org.projectlombok:lombok")
}
Version Catalog (Gradle)
# gradle/libs.versions.toml
[versions]
spring-boot = "3.3.0"
testcontainers = "1.19.0"
[libraries]
spring-boot-starter = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
testcontainers = { module = "org.testcontainers:testcontainers", version.ref = "testcontainers" }
Language-Specific Patterns
Records (Java 16+)
// Immutable data carrier — replaces Lombok @Data, manual POJOs
public record Order(Long id, String customer, BigDecimal total, OrderStatus status) {
// Compact constructor for validation
public Order {
if (total.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("total must be positive");
}
if (status == null) status = OrderStatus.PENDING;
}
// Static factory
public static Order create(String customer, BigDecimal total) {
return new Order(null, customer, total, OrderStatus.PENDING);
}
}
Sealed Classes (Java 17+)
// Exhaustive hierarchy — compiler knows all subtypes
public sealed interface Payment
permits CreditCard, PayPal, CryptoPayment {}
public record CreditCard(String lastFour, String expiry) implements Payment {}
public record PayPal(String email) implements Payment {}
public record CryptoPayment(String walletAddress, String currency) implements Payment {}
// Switch with pattern matching (Java 21+)
public String processPayment(Payment payment) {
return switch (payment) {
case CreditCard c -> "Processing card ending in " + c.lastFour();
case PayPal p -> "Redirecting to PayPal for " + p.email();
case CryptoPayment cp -> "Verifying " + cp.currency() + " transfer";
};
}
Streams & Optional
// Stream pipeline
List<String> activeCustomerEmails = orders.stream()
.filter(Order::isPaid)
.map(Order::customer)
.distinct()
.sorted()
.toList(); // Java 16+ — no need for .collect(Collectors.toList())
// Optional usage
public Customer findCustomer(Long id) {
return repository.findById(id)
.orElseThrow(() -> new CustomerNotFoundException(id));
}
// Optional chaining
String city = customer
.flatMap(Customer::getAddress)
.map(Address::getCity)
.orElse("Unknown");
CompletableFuture
public CompletableFuture<OrderResult> processOrderAsync(OrderRequest request) {
return CompletableFuture
.supplyAsync(() -> validateOrder(request), executor)
.thenCompose(validated ->
CompletableFuture.supplyAsync(() -> saveOrder(validated), executor))
.thenApplyAsync(this::enrichResult, executor)
.exceptionally(throwable -> {
log.error("Order processing failed", throwable);
return OrderResult.failed(throwable.getMessage());
});
}
// Combine multiple async results
CompletableFuture<Void> all = CompletableFuture.allOf(f1, f2, f3);
all.thenRun(() -> { /* all complete */ });
Testing & Tooling
JUnit 5
import org.junit.jupiter.api.*;
class OrderServiceTest {
@Test
void createOrder_setsPendingStatus() {
var order = service.createOrder(customerId, items);
assertEquals(OrderStatus.PENDING, order.status());
}
@ParameterizedTest
@CsvSource({ "1, 10.0", "2, 20.0", "5, 50.0" })
void calculateTotal_multipliesQuantity(int qty, double expected) {
assertEquals(BigDecimal.valueOf(expected), service.calculateTotal(item, qty));
}
@TestFactory
Stream<DynamicTest> dynamicTests() {
return testCases.stream()
.map(tc -> DynamicTest.dynamicTest(tc.name(), () -> {
assertEquals(tc.expected(), service.process(tc.input()));
}));
}
}
Spring Boot Test
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired private MockMvc mockMvc;
@Test
void createOrder_returns201() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("""
{"customerId": 1, "items": [{"productId": 1, "qty": 2}]}
"""))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id").isNumber());
}
}
Tooling
# Build and test
./mvnw clean verify # Maven wrapper (no local install needed)
./gradlew build # Gradle wrapper
# JVM tuning
java -Xms512m -Xmx2g -XX:+UseZGC -jar app.jar # ZGC for low-latency GC
# Profiling
jcmd <pid> VM.native_memory # Native memory tracking
jmap -dump:live,format=b <pid> # Heap dump
jstack <pid> # Thread dump
async-profiler -e cpu -d 30 -f profile.jfr
Anti-Patterns
- Raw
nullreturns: Forces callers to null-check. UseOptionalfor possibly-empty returns - Checked exceptions overused: Callers forced to catch or declare. Use unchecked exceptions for non-recoverable
synchronizedon hot methods: Contention kills throughput. UseReentrantLock,StampedLock, orConcurrentHashMap- God class with 5000+ lines: Impossible to test, modify, or understand. Split by responsibility
System.out.printlnin production code: Use SLF4J + Logback/Log4j2 with proper levelsnew Date()/System.currentTimeMillis()in tight loops: Creates objects. Cache or useInstant.now()- Double-checked locking without
volatile: Broken without volatile on the field. UseThread-safeholder pattern Vector,Hashtable,StringBuffer: Legacy synchronized classes — useArrayList,HashMap,StringBuilderfinalize(): Deprecated, unpredictable. UseCleaneror try-with-resources- No explicit charset in String.getBytes(): Platform-dependent. Always
getBytes(StandardCharsets.UTF_8)
Performance Patterns
StringBuilderfor concatenation in loops (not+operator)recordfor lightweight data carriers (no boilerplate, value-based)ConcurrentHashMapover synchronized maps for concurrent accessZGCorShenandoahfor sub-millisecond GC pauses (Java 17+)- Flyweight pattern for repeated small objects (Integer cache, custom pools)
- Off-heap storage with
ByteBuffer.allocateDirectfor large caches varkeyword doesn't affect performance but reduces noise- Profile with async-profiler before optimizing — JVM is already very good
- Use
--enable-previewin development to test upcoming features (pattern matching, string templates)
Virtual Threads & Structured Concurrency (Java 21+)
Virtual threads (Project Loom) enable lightweight concurrency without the complexity of reactive frameworks. Create via Thread.ofVirtual().start(task) or Executors.newVirtualThreadPerTaskExecutor(). Each virtual thread is a Java object (200 bytes), not an OS thread (1MB). Use for I/O-bound workloads: HTTP calls, database queries, file I/O. Structured Concurrency (StructuredTaskScope) ensures all subtasks complete or are cancelled together. Usage: try (var scope = new StructuredTaskScope.ShutdownOnFailure()) creates a scope, scope.fork(callable) submits tasks, scope.join() waits for all, scope.throwIfFailed() propagates first error, scope.result() returns successful results. Anti-pattern: pinning virtual threads with synchronized blocks. Use ReentrantLock when virtual threads are involved. Pin detection: JVM logs "Thread X pinned" — investigate and fix.
Modern Bean Validation & Error Handling
Use Jakarta Bean Validation 3.0+ for input validation instead of manual if/else chains. Annotations on records/POJOs: @NotBlank, @Email, @Positive, @Size(min=1, max=100), @Pattern(regexp="..."). Group validation: separate Create vs Update validation rules. Custom validators: implement ConstraintValidator<Annotation, Type>. Error handling with @ControllerAdvice (Spring) or ExceptionMapper (JAX-RS): return structured error responses with field-level errors. Pattern: validation exception → MethodArgumentNotValidException → extract field errors → ErrorResponse with code, message, fieldErrors list. Never return raw stack traces or Hibernate validation errors to clients.
Testing Strategy Beyond JUnit
Modern Java testing requires multiple layers: (a) unit tests with JUnit 5 + AssertJ (fluent assertions), (b) Mockito for mock dependencies, (c) @SpringBootTest for integration tests with real DB (Testcontainers), (d) @WebMvcTest for controller slice tests, (e) @DataJpaTest for repository tests. Testcontainers: write integration tests against real PostgreSQL/MySQL/Redis in Docker containers, not in-memory databases that behave differently. Use @Testcontainers annotation and @Container fields. Performance: use @SpringBootTest(webEnvironment = NONE) to avoid starting the full server for non-HTTP tests. Test slicing: @JsonTest for JSON serialization, @RestClientTest for REST clients. Mockito best practice: verify interactions, don't just stub — verify(repository).save(expectedOrder).
JVM Garbage Collection Strategy
| GC | Java Version | Best For | Pause Time |
|---|---|---|---|
| G1GC | 9+ (default 9-21) | General purpose, balanced | <10ms |
| ZGC | 15+ (incubating 11-14) | Large heaps (>100GB), low latency | <1ms |
| Shenandoah | 15+ (incubating 12-14) | Concurrent compaction | <1ms |
| Serial | All | Small heaps, single core | Longer pauses |
| Parallel | All (default 8) | Throughput, batch processing | Longer pauses |
GC flags for modern apps: -XX:+UseZGC -XX:MaxHeapSize=2g -XX:ParallelGCThreads=4 -XX:ConcGCThreads=2. Monitor GC with -Xlog:gc*:file=gc.log:time,uptime,tags and visualize with GCeasy or GCViewer. Heap dump on OOM: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/dump.hprof. Memory leak detection: take 3 heap dumps at intervals, compare with Eclipse MAT for growing object graphs.
Production Decision Trees
Build tool for greenfield project?
├── Spring Boot project → Gradle (Kotlin DSL) — faster incremental builds
├── Standard enterprise → Maven (stable, predictable)
└── Android → Gradle (Groovy or Kotlin DSL)
Microservices framework?
├── Full Spring ecosystem → Spring Boot + Spring Cloud
│ Service discovery: Eureka / Consul
│ Config: Spring Cloud Config / Vault
├── Low-resource, fast startup → Quarkus
│ GraalVM native: <100ms startup, <10MB RSS
│ Suitable for: serverless, high-density containers
├── Reactive, non-blocking → Micronaut
│ AOT compilation, compile-time DI
│ Suitable for: streaming, real-time
└── Minimal, no framework → JAX-RS + Helidon SE
Java version for new project?
├── Latest LTS (21) → All modern features
│ Virtual threads, records, sealed classes, pattern matching, string templates
├── Current non-LTS (22, 23) → Cutting edge
│ Statement before super, unnamed patterns, scoped values
└── Older LTS (17) → Widespread ecosystem support
Most libraries target 17+
OpenAPI & API-First Development
Springdoc: generate OpenAPI 3.0 spec from Spring Boot controllers via springdoc-openapi-starter-webmvc-ui. API-first: write OpenAPI spec first, generate server interfaces and client libraries with OpenAPI Generator. Annotations: @Operation(summary, description), @ApiResponse(...), @Schema(description). Group endpoints by tags. Generate Spring Boot interfaces: openapi-generator generate -g spring -i spec.yaml -o api/. The generated interfaces have the request mapping annotations; your implementation classes implement these interfaces — guarantees spec compliance.
Testing Strategy Decision Tree
What layer are you testing?
├── Unit (pure logic) → JUnit 5 + AssertJ
│ Fast (<100ms per test), no infrastructure
│ Test: validation, calculations, transformation, business rules
├── Service (business logic) → JUnit 5 + Mockito
│ Mock repositories, external clients, messaging
│ Verify: service methods, exception handling, transaction boundaries
├── Repository (data access) → @DataJpaTest + Testcontainers
│ Real PostgreSQL/MySQL in Docker, not H2
│ Test: queries, projections, pagination, native queries
├── Controller (HTTP) → @WebMvcTest + MockMvc
│ Slice test: only web layer, mock services
│ Test: serialization, validation, status codes, error responses
└── Integration → @SpringBootTest + Testcontainers + @RestClientTest
Full context, real DB, real message broker
Test: end-to-end flows, transaction rollback, async operations
Error Handling Pattern Comparison
How to return errors to client?
├── @ControllerAdvice (Spring) → Centralized exception handler
│ @ExceptionHandler per exception type
│ Returns ErrorResponse: code, message, timestamp, path, fieldErrors
├── ExceptionMapper (JAX-RS) → Same concept for Jakarta REST
│ ExceptionMapper<WebApplicationException> implementation
├── Result type (functional, no exceptions) → sealed interface Result<T, E>
│ Match on Success/Error cases, no try/catch branches
│ Good for: domain-driven design, functional modules
└── Problem+JSON (RFC 9457) → Standard error format for HTTP APIs
type, title, status, detail, instance, errors (custom)
Spring 6+ / Micronaut have built-in support
Error Response Schema (RFC 9457 Problem+JSON)
{
"type": "https://api.example.com/errors/order-not-found",
"title": "Order Not Found",
"status": 404,
"detail": "Order with ID 12345 does not exist",
"instance": "/api/orders/12345",
"errors": {
"orderId": "must be a positive integer"
}
}
Performance Optimization (Expanded)
recordfor DTOs: Immutable, no boilerplate, value-based equality. 40% less bytecode than equivalent POJO.- Stream API overhead: For small collections (<100), traditional for-loop is 2-3x faster. Use streams for readability at scale.
StringBuilderin loops:a + b + ccompiles tonew StringBuilder().append(a).append(b).append(c)— fine for 2-3 concats. Use explicitStringBuilderfor loops.ConcurrentHashMap.computeIfAbsent: Thread-safe memoization pattern. More efficient thanputIfAbsentfor expensive computations.- Connection pooling: HikariCP (default Spring Boot) — set
maximumPoolSize=10-20for most apps. Benchmark to find optimal. byte[]vsList<Byte>: Primitive arrays are 8x more memory efficient than boxed collections.ThreadLocalfor non-thread-safe resources:SimpleDateFormat,Random,DateTimeFormatter— reuse instances via ThreadLocal instead of creating per-call.-XX:+UseZGCfor low latency: Sub-millisecond GC pauses. Best for latency-sensitive services. Slightly more CPU than G1GC.-XX:+AlwaysPreTouch: Pre-allocate and touch all heap pages at startup. Slower startup but consistent runtime performance.- Record classes as DTOs: Endorsed by Spring/Jackson — no special config needed for serialization.
Anti-Patterns (Expanded)
@Autowiredfield injection: Makes testing with Mockito difficult (final fields, no constructor). Use constructor injection.@Dataon JPA entities: Lombok@Datageneratesequals()/hashCode()based on all fields — causes issues with lazy loading proxies and bidirectional relationships.- Spring
@Transactionalon private methods: Spring applies transaction via proxy — private methods bypass the proxy. Only public methods can be@Transactional. Thread.sleep()for async coordination: Fragile, slow, flaky. UseCountDownLatch,CompletableFuture, orAwaitility.- Handling
InterruptedExceptionpoorly: Swallowing orThread.currentThread().interrupt()incorrectly. Restore interrupt flag:Thread.currentThread().interrupt()and re-throw or return. instanceofchecks after pattern matching (Java 16+): Switch pattern matching andif (x instanceof Foo f)eliminate mostinstanceofchains. Use them.java.util.Dateandjava.util.Calendar: Legacy, mutable, confusing. Usejava.time.*API (Java 8+):Instant,LocalDate,ZonedDateTime,Duration.@RequestMappingon field instead of method: Controller field-level mapping is confusing. Each method should have explicit@GetMapping/@PostMapping.- Reading
InputStreamwithout closing: Always use try-with-resources forInputStream,Connection,Statement,ResultSet. - Static utility classes calling Spring beans: Static methods can't be mocked. Make utility classes injectable beans or pass dependencies as parameters.
Reactive Programming (Spring WebFlux)
For high-throughput, low-latency services, use WebFlux with Project Reactor. Key types: Mono<T> (0-1 items) and Flux<T> (0-N items). Operators: .map(), .flatMap(), .filter(), .zipWith(), .retry(), .timeout(). Backpressure: subscriber controls data flow rate. Database: R2DBC (reactive SQL), MongoDB Reactive Streams, Redis Reactive. Anti-patterns: (a) blocking call in reactive pipeline (.block() defeats the purpose), (b) not subscribing (nothing happens without subscriber), (c) shared mutable state, (d) long computations on the event loop (use .publishOn(Schedulers.boundedElastic())). WebClient over RestTemplate for all HTTP calls. Trace with reactor.core.publisher.Hooks.onOperatorDebug().
Code Examples — Virtual Threads
// Java 21+ — Virtual thread HTTP client
var executor = Executors.newVirtualThreadPerTaskExecutor();
List<CompletableFuture<String>> futures = urls.stream()
.map(url -> CompletableFuture.supplyAsync(() -> fetchUrl(url), executor))
.toList();
var results = futures.stream()
.map(CompletableFuture::join)
.toList();
// Structured concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> fetchUser(id));
Future<List<Order>> orders = scope.fork(() -> fetchOrders(id));
scope.join();
scope.throwIfFailed();
return new UserDashboard(user.resultNow(), orders.resultNow());
}
References
references/streams-optional.md— Stream API, Optional patternsreferences/records-sealed-classes.md— Records, sealed classes, pattern matchingreferences/java-fundamentals.md— Java Fundamentalsreferences/java-advanced.md— Advanced Java Patternsreferences/java-testing.md— Java Testing Guide
Implementation Patterns
Pattern: Repository with Specification
public interface OrderRepository extends JpaRepository<Order, Long> {
@Query("""
SELECT o FROM Order o
WHERE o.customerId = :customerId
AND o.status IN :statuses
AND o.createdAt BETWEEN :from AND :to
""")
List<Order> findFiltered(Long customerId, List<OrderStatus> statuses, Instant from, Instant to);
}
// Specification pattern for dynamic queries
public class OrderSpecifications {
public static Specification<Order> byCustomer(Long customerId) {
return (root, query, cb) -> cb.equal(root.get("customerId"), customerId);
}
public static Specification<Order> byStatus(OrderStatus... statuses) {
return (root, query, cb) -> root.get("status").in((Object[]) statuses);
}
public static Specification<Order> createdBetween(Instant from, Instant to) {
return (root, query, cb) -> cb.between(root.get("createdAt"), from, to);
}
}
Pattern: Async Event Publisher with Retry
@Component
public class DomainEventPublisher {
private final ApplicationEventPublisher publisher;
private final RetryTemplate retryTemplate;
public void publish(DomainEvent event) {
retryTemplate.execute(ctx -> {
publisher.publishEvent(event);
return null;
});
}
}
// RetryTemplate configuration
@Bean
public RetryTemplate retryTemplate() {
return RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(100, 2, 2000)
.retryOn(TransientDataAccessException.class)
.build();
}
Production Considerations
JVM Tuning for Containers
-XX:+UseContainerSupport(default in JDK 10+) — JVM detects container CPU/memory limits.-XX:InitialRAMPercentage=50 -XX:MaxRAMPercentage=75— heap relative to container memory.-XX:+ExitOnOutOfMemoryError— container orchestrator restarts failed process.-Xlog:gc*:file=gc.log:time,uptime,tags— GC logging for post-mortem analysis.- Heap dump on OOM:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dumps/.
Observability
- Micrometer + MicrometerRegistry for metrics. Export to Prometheus via
micrometer-registry-prometheus. - Spring Actuator:
/actuator/health(liveness + readiness),/actuator/metrics,/actuator/prometheus. - Distributed tracing: Micrometer Tracing + Brave + Zipkin. Propagate trace IDs via HTTP headers.
- Structured logging: Logback with JSON encoder.
net.logstash.logback.encoder.LogstashEncoder.
Anti-Patterns
| Anti-Pattern | Why It Hurts | Fix |
|---|---|---|
@RequestMapping on class-level with no method narrowing |
All requests map to same path. Confusing. | Use @GetMapping, @PostMapping per method |
Thread.sleep() in tests |
Flaky, slow. | Awaitility.await().atMost(5, SECONDS).until(...) |
Overusing Optional.get() |
Still throws NoSuchElementException. | orElseThrow() with domain-specific exception |
Catch Exception broadly |
Swallows critical errors like InterruptedException. | Catch specific exception types |
Mutable fields in @Entity |
Hibernate dirty checking scans all fields. | Use @Immutable or DTO projections |
| Static methods calling Spring beans | Can't mock in unit tests. | Inject as bean dependency |
Performance Optimization
- Virtual threads for I/O:
Executors.newVirtualThreadPerTaskExecutor()— 20x more throughput than platform threads. - Record classes for DTOs: 40% less bytecode than POJOs. Value-based equality.
ConcurrentHashMap.computeIfAbsent(k, fn)— atomic memoization. AvoidsputIfAbsentrace.- String pooling with
String.intern()only for small, bounded sets of strings. - Jackson performance: use
ObjectMapperas singleton. Register Kotlin/Java time modules once. - HikariCP pool sizing:
maximumPoolSize = ((core_count * 2) + effective_spindle_count). Start with 10, tune up. - Query tuning:
@QueryHints(@QueryHint(name = "org.hibernate.readOnly", value = "true"))for read-only queries.
Security Considerations
- Spring Security:
SecurityFilterChainwith explicit permitAll vs authenticated. Never role-based on global permit. - CSRF: enabled by default in Spring Security. Stateless APIs use CSRF disabled + JWT.
- CORS:
@CrossOrigin(origins = "https://app.example.com"). Neverorigins = "*". - JWT:
nimbus-jose-jwtfor parsing. Validate signature, issuer, audience, expiration. Reject alg:none. - SQL injection: JPA/Hibernate parameterized queries. Native queries use
?or:namedplaceholders. - Input validation:
@Validon controller params with Jakarta Validation annotations. - Rate limiting: Bucket4j or Spring Cloud Gateway's
RequestRateLimiterfilter. - Secrets: Spring Cloud Config with Vault backend. Or
@Valuefrom environment variables. Never hardcoded. - Dependency scanning: OWASP Dependency Check in CI. Fail builds on CVSS >= 7.0.