Java & Kotlin
Modern Java features, Spring Boot essentials, JVM internals, Kotlin advantages, and enterprise Java in New Zealand
Java & Kotlin
Java is not exciting. That is its strength. It is the language that runs the banking systems, insurance platforms, and enterprise backends that handle real money and real consequences. Modern Java (17+) is dramatically better than the Java of ten years ago -- records, sealed classes, pattern matching, and virtual threads have closed many of the gaps that drove people to other languages.
Kotlin sits on the same JVM and offers everything Java developers wish Java had: null safety, coroutines, data classes, extension functions, and concise syntax. The two languages interoperate seamlessly, and many teams adopt Kotlin incrementally alongside existing Java codebases.
Modern Java (17+)
Records
Records replace the boilerplate of POJOs. They are immutable data carriers with auto-generated equals(), hashCode(), and toString():
// Before: 40+ lines of boilerplate
// After: one line
public record User(String id, String name, String email, Instant createdAt) {}
// Usage
var user = new User("u1", "Alice", "alice@example.com", Instant.now());
System.out.println(user.name()); // "Alice"
// Records can have custom constructors for validation
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
Objects.requireNonNull(currency, "Currency is required");
}
}Sealed Classes
Sealed classes restrict which classes can extend them, enabling exhaustive pattern matching:
public sealed interface Shape
permits Circle, Rectangle, Triangle {}
public record Circle(double radius) implements Shape {}
public record Rectangle(double width, double height) implements Shape {}
public record Triangle(double base, double height) implements Shape {}
// The compiler knows all subtypes -- exhaustive matching
public double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius() * c.radius();
case Rectangle r -> r.width() * r.height();
case Triangle t -> 0.5 * t.base() * t.height();
// No default needed -- compiler verifies exhaustiveness
};
}Pattern Matching
Pattern matching in switch and instanceof eliminates casting boilerplate:
// Pattern matching for instanceof
public String describe(Object obj) {
if (obj instanceof String s && s.length() > 5) {
return "Long string: " + s;
}
if (obj instanceof Integer i && i > 0) {
return "Positive integer: " + i;
}
return "Unknown: " + obj;
}
// Switch expressions with guards
public String classify(Shape shape) {
return switch (shape) {
case Circle c when c.radius() > 100 -> "large circle";
case Circle c -> "small circle";
case Rectangle r when r.width() == r.height() -> "square";
case Rectangle r -> "rectangle";
case Triangle t -> "triangle";
};
}Virtual Threads (Project Loom)
Virtual threads are the biggest change to Java concurrency in 20 years. They make blocking I/O efficient without the complexity of reactive programming:
// Create millions of virtual threads -- each costs ~1KB vs ~1MB for platform threads
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> futures = urls.stream()
.map(url -> executor.submit(() -> fetchUrl(url)))
.toList();
List<String> results = futures.stream()
.map(f -> {
try { return f.get(); }
catch (Exception e) { return "error: " + e.getMessage(); }
})
.toList();
}
// In Spring Boot 3.2+, enable virtual threads with one property:
// spring.threads.virtual.enabled=true
// Every request handler now runs on a virtual thread automaticallySpring Boot Essentials
Spring Boot remains the dominant Java framework. These are the concepts that matter most.
Dependency Injection
@Service
public class OrderService {
private final OrderRepository orderRepo;
private final PaymentClient paymentClient;
private final NotificationService notifications;
// Constructor injection -- preferred over @Autowired on fields
public OrderService(
OrderRepository orderRepo,
PaymentClient paymentClient,
NotificationService notifications) {
this.orderRepo = orderRepo;
this.paymentClient = paymentClient;
this.notifications = notifications;
}
@Transactional
public Order placeOrder(CreateOrderRequest request) {
var order = orderRepo.save(Order.from(request));
paymentClient.charge(order.total(), request.paymentMethod());
notifications.sendConfirmation(order);
return order;
}
}Profiles and Configuration
// application.yml
// spring:
// profiles:
// active: ${SPRING_PROFILES_ACTIVE:dev}
// application-dev.yml -- local development
// application-prod.yml -- production
@Configuration
@Profile("prod")
public class ProductionConfig {
@Bean
public DataSource dataSource(
@Value("${db.url}") String url,
@Value("${db.username}") String username,
@Value("${db.password}") String password) {
var ds = new HikariDataSource();
ds.setJdbcUrl(url);
ds.setUsername(username);
ds.setPassword(password);
ds.setMaximumPoolSize(20);
return ds;
}
}Actuator for Production
Spring Boot Actuator provides production-ready endpoints out of the box:
# application.yml
management:
endpoints:
web:
exposure:
include: health, metrics, info, prometheus
endpoint:
health:
show-details: when-authorized
health:
db:
enabled: true
redis:
enabled: trueKey endpoints: /actuator/health for liveness/readiness probes, /actuator/prometheus for metrics scraping, /actuator/info for build metadata.
JVM Memory Model and GC
Understanding JVM memory is essential for diagnosing production issues.
┌─────────────────────────────────────────┐
│ JVM Heap │
│ ┌──────────┐ ┌──────────────────────┐ │
│ │ Young │ │ Old Generation │ │
│ │ Gen │ │ │ │
│ │ Eden + S0 │ │ Long-lived objects │ │
│ │ + S1 │ │ │ │
│ └──────────┘ └──────────────────────┘ │
├─────────────────────────────────────────┤
│ Metaspace (class metadata, off-heap) │
├─────────────────────────────────────────┤
│ Thread stacks (per-thread, off-heap) │
└─────────────────────────────────────────┘GC Tuning Basics
For most services, start with these and adjust based on metrics:
# G1GC (default since Java 9) -- good for most workloads
java -Xms512m -Xmx2g \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:+HeapDumpOnOutOfMemoryError \
-jar app.jar
# ZGC -- for low-latency services (sub-millisecond pauses)
java -Xms1g -Xmx4g \
-XX:+UseZGC \
-jar app.jarRules of thumb:
- Set
-Xmsequal to-Xmxin production to avoid heap resizing - Enable
-XX:+HeapDumpOnOutOfMemoryErroralways - Use G1GC unless you have a specific reason not to
- ZGC for services where P99 latency matters more than throughput
Kotlin
Null Safety
Kotlin's null safety eliminates NullPointerException at compile time:
// Non-nullable by default
var name: String = "Alice"
name = null // Compile error
// Nullable requires explicit ?
var email: String? = null
// Safe call operator
val length: Int? = email?.length
// Elvis operator for defaults
val displayName: String = email ?: "anonymous"
// Smart casting after null check
fun greet(user: User?) {
if (user != null) {
// user is automatically cast to non-null User here
println("Hello, ${user.name}")
}
}Coroutines
Kotlin coroutines provide structured concurrency -- simpler than Java's virtual threads for complex async workflows:
suspend fun fetchUserProfile(userId: String): UserProfile = coroutineScope {
val user = async { userService.getUser(userId) }
val orders = async { orderService.getOrders(userId) }
val preferences = async { preferenceService.get(userId) }
UserProfile(
user = user.await(),
orders = orders.await(),
preferences = preferences.await()
)
}
// Structured concurrency: if any child fails, all are cancelled
// Timeout support built in
suspend fun fetchWithTimeout(userId: String): UserProfile =
withTimeout(5.seconds) {
fetchUserProfile(userId)
}Data Classes and Extension Functions
// Data class: like Java records but with copy()
data class User(
val id: String,
val name: String,
val email: String,
val role: Role = Role.USER
)
val user = User("u1", "Alice", "alice@example.com")
val admin = user.copy(role = Role.ADMIN) // Immutable update
// Extension functions: add methods to existing types
fun String.toSlug(): String =
this.lowercase()
.replace(Regex("[^a-z0-9\\s-]"), "")
.replace(Regex("\\s+"), "-")
.trim('-')
val slug = "Hello World!".toSlug() // "hello-world"
// Scoping functions
val result = userRepository.findById(id)?.let { user ->
auditLog.record("accessed", user.id)
user.toPublicProfile()
} ?: throw NotFoundException("User $id not found")Kotlin vs Java: When to Use Which
| Aspect | Java | Kotlin |
|---|---|---|
| Null safety | Optional, annotations | Built into the type system |
| Conciseness | Verbose (improving) | Concise by design |
| Coroutines | Virtual threads (Java 21+) | First-class, structured |
| Data classes | Records (Java 16+) | Data classes with copy() |
| Extension functions | Not available | First-class feature |
| Pattern matching | Switch expressions (evolving) | when expressions, smart casts |
| Ecosystem maturity | Massive, decades of libraries | Full Java interop |
| Hiring pool | Very large | Smaller but growing |
| Learning curve | Well-known | Easy for Java developers |
Pragmatic advice: If starting a new JVM project, Kotlin is the better choice for developer productivity. If maintaining an existing Java codebase, modern Java (17+) is good enough -- do not rewrite for Kotlin's sake. If hiring is your bottleneck, Java has a larger candidate pool.
Enterprise Java in New Zealand
Java dominates NZ's enterprise sector:
- Banking: ANZ and Westpac run core systems on Java/Spring. These are large, long-lived codebases with strict reliability requirements.
- Insurance: Tower, Partners Life, and others rely on Java backends for policy management and claims processing.
- Government and public sector: Datacom and other contractors build government systems primarily in Java and C#.
- Xero: While Xero's newer services use Go and .NET, Java is part of their stack.
For senior roles in these organizations, deep Spring Boot knowledge, JVM tuning experience, and understanding of enterprise patterns (CQRS, event sourcing, saga patterns) are valued more than language novelty. Java may not be fashionable, but it pays well and the demand is consistent.