Java Backend Fundamentals
Review modern Java types, object design, collections, exceptions, JVM behavior, and concurrency through one backend domain example.
Learning objectives
- Choose Java types and collections from domain invariants
- Explain JVM memory and concurrency risks relevant to request processing
What it is
Java backend fundamentals connect language syntax to durable domain behavior: values and identity, equality, collections, failure boundaries, resource management, JVM execution, and safe concurrency. Framework annotations cannot compensate for weak understanding here.
Why it matters
A Spring service runs ordinary Java code on a JVM under concurrent requests. Incorrect equality can lose map entries, mutable objects can leak state, swallowed exceptions can commit partial work, and unbounded executors can exhaust memory or downstream capacity.
How it works
| Area | Backend question |
|---|---|
| Types | Is this value nullable, mutable, bounded, or identified? |
| OOP | Which invariant belongs inside the domain and which dependency belongs behind an interface? |
| Collections | Do order, uniqueness, lookup, equality, and concurrency matter? |
| Exceptions | Which layer can recover, translate, retry, or terminate? |
| JVM | Where are objects allocated and which roots keep them reachable? |
| Concurrency | Which state is shared and what establishes atomicity or ordering? |
Simple example
public record VoucherCode(String value) {
public VoucherCode {
value = Objects.requireNonNull(value, "value").trim();
if (!value.matches("[A-Z0-9]{12}")) {
throw new IllegalArgumentException("Invalid voucher code format");
}
}
}
enum VoucherStatus { ISSUED, REDEEMED, EXPIRED, CANCELLED }
sealed interface RedemptionResult
permits RedemptionResult.Accepted, RedemptionResult.Rejected {
record Accepted(long redemptionId) implements RedemptionResult {}
record Rejected(String reasonCode) implements RedemptionResult {}
}The types make invalid format and unknown result variants harder to represent. They do not by themselves make redemption atomic; the database transaction still owns that invariant.
Backend example
final class VoucherRules {
private final Set<String> permittedMerchantIds;
VoucherRules(Set<String> permittedMerchantIds) {
this.permittedMerchantIds = Set.copyOf(permittedMerchantIds);
}
RedemptionResult evaluate(Voucher voucher, String merchantId, Instant now) {
Objects.requireNonNull(now, "now");
if (!permittedMerchantIds.contains(merchantId)) {
return new RedemptionResult.Rejected("MERCHANT_NOT_ALLOWED");
}
if (!voucher.expiresAt().isAfter(now)) {
return new RedemptionResult.Rejected("VOUCHER_EXPIRED");
}
return new RedemptionResult.Accepted(voucher.id());
}
}Production example
JVM execution model
| Tool/evidence | Use |
|---|---|
| Thread dump | Blocked, waiting, deadlocked, and runnable thread state |
| Heap dump | Retained objects and suspected memory leaks |
| GC logs/JFR | Allocation, pause, CPU, lock, and I/O evidence |
| Pool metrics | Whether application concurrency exceeds downstream capacity |
Common mistakes
- Using == instead of equals for object value comparison
- Overriding equals without a consistent hashCode
- Returning mutable internal collections
- Catching Exception and continuing without a recovery policy
- Using Optional for fields or parameters as a default design
- Creating unbounded threads or CompletableFuture work on an unsuitable shared executor
Best practices
- Prefer composition and small cohesive types
- Make invariants explicit in constructors or factories
- Use try-with-resources for closeable resources
- Translate exceptions at stable boundaries and preserve the cause
- Choose collections by semantics before performance
- Measure JVM and concurrency behavior under realistic downstream limits
Trade-offs
| Choice | Use when | Risk |
|---|---|---|
| Checked exception | Callers are expected to handle a recoverable condition | Propagation boilerplate and accidental wrapping |
| Runtime exception | Invariant or boundary failure cannot be handled locally | Undocumented failure contracts |
| Inheritance | A true substitutable subtype relationship exists | Tight coupling and fragile hierarchies |
| Composition | Behavior can be delegated behind a focused interface | More small objects and wiring |
Interview questions
- What is the contract between equals and hashCode?
- How do stack frames and heap objects differ?
- Why is a record only shallowly immutable?
- What causes a Java memory leak despite garbage collection?
- When are virtual threads appropriate?
- How would you diagnose a deadlock?
Hands-on task
- 1
Model
Create voucher value types, lifecycle states, and sealed redemption outcomes.
- 2
Test
Write parameterized tests for format, expiry, merchant rules, equality, and mutable collection defenses.
- 3
Stress
Run concurrent evaluation tasks, capture a thread dump, and explain which shared state is safe.
References
- Oracle: JDK 25 Documentation — https://docs.oracle.com/en/java/javase/25/
- Oracle: The Java Virtual Machine Specification, Java SE 25 — https://docs.oracle.com/javase/specs/jvms/se25/html/
- Oracle: Thread API — Virtual Threads — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Thread.html
Retrieval check
Before you continue
- Explain the main idea in your own words without rereading the page.
- Name one production problem this knowledge helps prevent.
- Describe where you would apply it in a Spring Boot project.
Source metadata
Primary references checked on 2026-07-16. Version scope is stated at the top of this page.