Foundation · Prepare
Core Java & Backend Prerequisites
Refresh Java types, variables, conditionals, loops, methods, and the object-model Spring will automate around one Task use case.
Primary source: Java Language Specification: Records and Pattern Matching
Lesson 2 of 7
1d streakThe Architectural Question
Most Spring confusion is ordinary Java or protocol confusion in disguise: a null dependency is a construction mistake, and a 400 response is an HTTP contract question.
What this guide proves
- OOP and interfaces
- Collections, generics, and exceptions
- HTTP, SQL, and build basics
Refresh the Java syntax you need
This is a quick revision, not a complete Java course. You should recognize the basic shapes that appear in backend code: primitive values such as int, long, double, boolean, and char; reference values such as String and List; variables, conditions, loops, and methods. If any of these feels unfamiliar, pause here and practice the small example before continuing.
final class JavaSyntaxRefresh {
static int countOpen(String[] statuses) {
int openCount = 0;
for (String status : statuses) {
if ("OPEN".equals(status)) {
openCount++;
}
}
return openCount;
}
}This example deliberately stays small: it uses an array, a String, an int variable, a condition, an enhanced for loop, and a method. The goal is to recognize Java syntax, not to teach collections, streams, money, or exception design yet.
| Shape | Purpose | Example |
|---|---|---|
| Primitive | Stores a basic value | int count = 3; |
| Reference | Refers to an object | String title = "Read"; |
| Condition | Chooses a path | if (count > 0) { ... } |
| Loop | Repeats work | for (String item : items) { ... } |
| Method | Names reusable behavior | countOpen(statuses) |
Use this as a checkpoint
Active Recall Checkpoint
What should you be able to explain before moving on?
Show answer & explanation
What type the method receives, what the loop repeats, when the condition matches, how openCount changes, and what value the method returns. Use the dedicated Core Java lessons for a deeper gap.
Start with an ordinary Java class model
Spring Boot does not change Java semantics. A service is still an object, a dependency is still another object it collaborates with, and a request eventually becomes an ordinary method call. Before learning annotations, you should be able to read a small object graph and identify which object owns each rule. The Task example starts with a normal class so you can see fields, constructors, methods, and encapsulation before records are introduced.
enum TaskStatus { OPEN, COMPLETED }
final class Task {
private final long id;
private final String title;
private final TaskStatus status;
Task(long id, String title, TaskStatus status) {
if (id <= 0) throw new IllegalArgumentException("id must be positive");
if (title == null || title.isBlank()) {
throw new IllegalArgumentException("title is required");
}
this.id = id;
this.title = title;
this.status = status;
}
long id() { return id; }
String title() { return title; }
TaskStatus status() { return status; }
Task complete() {
if (status == TaskStatus.COMPLETED) return this;
return new Task(id, title, TaskStatus.COMPLETED);
}
}The constructor protects the object at creation time, so downstream code can trust that every Task has a positive id and nonblank title. The fields are private and final, but this class is only shallowly immutable if it later contains mutable components. The complete method returns a new value instead of exposing a public setter. That is encapsulation: the type owns the transition and callers ask for an operation rather than manipulating its fields.
Active Recall Checkpoint
What does this class teach before records, and what would a record change?
Show answer & explanation
The class makes state and behavior explicit. A record would generate accessors, equality, and a canonical constructor for data-oriented values, but records are only shallowly immutable and still do not decide business rules for you.
Wire collaborators through constructors
A useful class should receive the collaborators it needs instead of constructing concrete infrastructure internally. This keeps the business operation independent of storage and makes the object graph visible at the program boundary. Spring calls this dependency injection, but the underlying Java is simply one constructor receiving another object.
import java.util.Optional;
interface TaskRepository {
Optional<Task> findById(long id);
Task save(Task task);
}
final class TaskNotFoundException extends RuntimeException {
TaskNotFoundException(long id) {
super("task " + id + " was not found");
}
}
final class TaskService {
private final TaskRepository tasks;
TaskService(TaskRepository tasks) {
this.tasks = tasks;
}
Task complete(long id) {
Task task = tasks.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
return tasks.save(task.complete());
}
}The interface describes what the use case needs: lookup and save. It does not promise SQL, JPA, or an in-memory map. Optional<Task> makes the missing case part of the method signature, and orElseThrow converts that absence into a domain-level failure. Later, Spring can supply a repository implementation to the same constructor without changing TaskService.
Keep the constructor explicit
Active Recall Checkpoint
What is wrong with returning tasks.findById(id) from a method declared to return Task?
Show answer & explanation
findById returns Optional<Task>, not Task. The method must deliberately handle absence by returning Optional, choosing a fallback, or throwing a named exception.
Read collections, generics, streams, and failures as flow
Backend signatures often carry more meaning in their types than in their method names. Read List<Task> as an ordered collection containing only Task values, Map<Long, Task> as a lookup from ids to tasks, and Optional<Task> as zero or one Task. Generics allow those guarantees to be checked by the compiler instead of rediscovered through casts at runtime.
import java.util.List;
List<String> openTitles(List<Task> tasks) {
return tasks.stream()
.filter(task -> task.status() == TaskStatus.OPEN)
.map(Task::title)
.toList();
}
}A stream is a pipeline over values: select open tasks, project each one to its title, and collect the results. It does not automatically make work parallel, faster, or database-backed. When a Spring Data method returns a collection or Optional, first understand the Java type; only then ask how the repository obtained it.
| Shape | Meaning | Question to ask |
|---|---|---|
Task | Exactly one value is promised | What happens if it cannot be produced? |
Optional<Task> | The value may be absent | Who chooses the missing-case behavior? |
List<Task> | Zero or more ordered values | Can callers mutate the returned list? |
Map<Long, Task> | Values indexed by a key | What guarantees key uniqueness? |
Exceptions carry failed control flow. Catch an exception only where you can add context, recover, translate it into another contract, or release a resource. Logging and rethrowing at every layer creates duplicates; swallowing a failure destroys the evidence needed to diagnose it. Later, an HTTP exception handler will translate TaskNotFoundException into a 404 response.
Active Recall Checkpoint
A repository returns Optional.empty(). Is that itself an HTTP 404?
Show answer & explanation
No. Optional is a Java representation of absence. The application or web boundary decides whether that absence becomes 404, an empty result, a new value, or another outcome.
Prove behavior with one focused test
A framework-free unit test should construct the object graph directly and observe a meaningful outcome. The fake repository below is intentionally small: it gives the service controllable state without pretending to be PostgreSQL. This test proves the use-case rule; later integration tests will separately prove database mapping and transaction behavior.
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
class TaskServiceTest {
@Test
void completes_an_open_task() {
var repository = new InMemoryTasks(
new Task(42, "Read Module 0", TaskStatus.OPEN));
var service = new TaskService(repository);
Task completed = service.complete(42);
assertEquals(TaskStatus.COMPLETED, completed.status());
assertEquals(TaskStatus.COMPLETED, repository.findById(42).orElseThrow().status());
}
static final class InMemoryTasks implements TaskRepository {
private final Map<Long, Task> data = new HashMap<>();
InMemoryTasks(Task task) { data.put(task.id(), task); }
public Optional<Task> findById(long id) {
return Optional.ofNullable(data.get(id));
}
public Task save(Task task) {
data.put(task.id(), task);
return task;
}
}
}The test contains arrange, act, and assert without requiring those labels. It controls the initial task, calls one public behavior, and checks both the returned value and saved state. If this shape feels unfamiliar, pause for the Core Java classes, interfaces, collections, generics, exception-handling, and modern-Java lessons linked from the course.
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
Use the readiness gate honestly
You are ready for Module 1 when you can explain the preceding program without framework vocabulary and change it safely. You do not need reflection internals, advanced concurrency, JVM tuning, or annotation processing yet. Those topics matter later, when a production symptom creates a reason to learn them.
- 01Trace JavaSyntaxRefresh: name the types, follow each conditional, and explain how the loop builds the returned value.
- 02Explain why
TaskServicedepends on an interface and where the concrete repository is chosen. - 03Read
Optional<Task>andList<String>aloud and describe the empty cases. - 04Add a failing test for an unknown task and predict the exception before running it.
- 05Explain which responsibility belongs to Java, which will belong to HTTP, and which will belong to SQL.
Do not substitute recognition for readiness
Active Recall Checkpoint
Should you repeat every Java topic before continuing?
Show answer & explanation
Only if the diagnostic exposes a gap. Use the dedicated Core Java lessons for the failed concept, then return and rerun this small Task example.
Guided Hands-On Lab
Code Walkthrough
Sketch a three-class object graph wired through constructors, then name the HTTP method and SQL statement that would sit behind one of its operations.
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
public class TaskReadiness {
enum TaskStatus { OPEN, COMPLETED }
// 1. Immutable domain model with constructor validation
static final class Task {
private final long id;
private final String title;
private final TaskStatus status;
private final Instant createdAt;
Task(long id, String title, TaskStatus status, Instant createdAt) {
if (id <= 0) throw new IllegalArgumentException("id must be positive");
if (title == null || title.isBlank()) throw new IllegalArgumentException("title is required");
this.id = id;
this.title = title;
this.status = status;
this.createdAt = createdAt;
}
long id() { return id; }
String title() { return title; }
TaskStatus status() { return status; }
Instant createdAt() { return createdAt; }
Task complete() {
if (status == TaskStatus.COMPLETED) return this;
return new Task(id, title, TaskStatus.COMPLETED, createdAt);
}
@Override
public String toString() {
return "Task[id=" + id + ", title=" + title + ", status=" + status + ", createdAt=" + createdAt + "]";
}
}
// 2. Storage abstraction
interface TaskRepository {
Optional<Task> findById(long id);
Task save(Task task);
}
static final class TaskNotFoundException extends RuntimeException {
TaskNotFoundException(long id) { super("task " + id + " was not found"); }
}
// 3. Constructor dependency injection (Zero annotations required)
static final class TaskService {
private final TaskRepository tasks;
TaskService(TaskRepository tasks) { this.tasks = tasks; }
Task complete(long id) {
Task task = tasks.findById(id)
.orElseThrow(() -> new TaskNotFoundException(id));
return tasks.save(task.complete());
}
}
public static void main(String[] args) {
Map<Long, Task> db = new HashMap<>();
db.put(42L, new Task(42, "Read Module 0", TaskStatus.OPEN, Instant.now()));
TaskRepository repo = new TaskRepository() {
public Optional<Task> findById(long id) { return Optional.ofNullable(db.get(id)); }
public Task save(Task task) { db.put(task.id(), task); return task; }
};
TaskService service = new TaskService(repo);
// Assert 1: Happy path completion
Task done = service.complete(42);
assert done.status() == TaskStatus.COMPLETED : "Expected COMPLETED";
System.out.println("[PASS] completes_an_open_task: " + done);
// Assert 2: Absence handling converts Optional.empty() to domain exception
try {
service.complete(99);
assert false : "Expected TaskNotFoundException";
} catch (TaskNotFoundException e) {
System.out.println("[PASS] throws_on_missing_task: " + e.getMessage());
}
// Assert 3: Input invariant validation in Task constructor
try {
new Task(-1, "Invalid", TaskStatus.OPEN, Instant.now());
assert false : "Expected IllegalArgumentException";
} catch (IllegalArgumentException e) {
System.out.println("[PASS] rejects_negative_id: " + e.getMessage());
}
}
}Guided LabBuild & Verify Step by Step
Java 21 LTS environment with TaskReadiness.java. Plain Java without Spring annotations.
- 01
Test constructor injection & immutability
Compile and run TaskReadiness.java with assertions enabled (-ea). Verify that completing a task produces a new Task instance with COMPLETED status.
Check: The first test passes: [PASS] completes_an_open_task.
- 02
Verify absence handling
Call service.complete(99) for an absent identifier and confirm that Optional.orElseThrow converts absence to a domain exception.
Check: The second test passes: [PASS] throws_on_missing_task: task 99 was not found.
- 03
Verify constructor validation
Attempt to construct a Task with a negative id (-1) and confirm that the Task constructor throws IllegalArgumentException.
Check: The third test passes: [PASS] rejects_negative_id: id must be positive.
Command
javac --release 21 TaskReadiness.java java -ea TaskReadiness
Expected Output
[PASS] completes_an_open_task: Task[id=42, title=Read Module 0, status=COMPLETED, createdAt=...] [PASS] throws_on_missing_task: task 99 was not found [PASS] rejects_negative_id: id must be positive
[INFO] Scanning for projects... [INFO] -------------------< com.example:order-service >------------------- [INFO] Building order-service 0.0.1-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-surefire-plugin:3.2.5:test (default-test) @ order-service --- [INFO] Running com.example.OrderServiceTest [ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.284 s <<< FAILURE! -- in com.example.OrderServiceTest [ERROR] com.example.OrderServiceTest.testOrderCreation -- Time elapsed: 0.041 s <<< ERROR! java.lang.NullPointerException: Cannot invoke "com.example.OrderRepository.save(Object)" because "this.repository" is null at com.example.OrderService.createOrder(OrderService.java:18) at com.example.OrderServiceTest.testOrderCreation(OrderServiceTest.java:24) [INFO] Results: [INFO] [ERROR] Errors: [ERROR] OrderServiceTest.testOrderCreation:24 » NullPointer Cannot invoke "com.example... [INFO] [ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:3.2.5:test on project order-service: There are test failures. [ERROR] -> [Help 1] [ERROR] BUILD FAILURE
Synthesis & Production Verification
Deliberate Failure Condition
- Break: Start learning annotations before understanding interfaces, constructors, collections, exceptions, HTTP, SQL, or the classpath
- Observe: Every failure reads as framework magic because there is no plain-Java model to compare it against
- Repair: Diagnose the owning concept first—Java semantics, HTTP, SQL, or the classpath—then map it to the Spring feature that automates it.
Production Acceptance Criteria
- Explain classes, records, interfaces, and constructor dependencies without a framework
- Read signatures that use generics, collections, and Optional without guessing
- Describe what the build tool compiles and packages versus what HTTP and SQL guarantee
Key Takeaway
Every Spring mechanism is ordinary Java underneath: objects constructed with their dependencies, collections and generics in signatures, exceptions reporting failure. If you can read plain Java fluently, Spring becomes automation instead of magic.
Architectural & Technical Review
Core Interview Questions
Q01Why do backend teams prefer constructor injection over assigning fields after construction?Show answer
A constructor that demands its dependencies makes it impossible to create a half-wired object: the class cannot be instantiated without everything it needs, and the fields can be final. Setting fields afterwards allows an object to exist in an invalid state.
Q02When should a Java API return Optional instead of null?Show answer
Return Optional when absence is an expected result that the caller must handle, such as a repository lookup. Do not call get without checking; use orElseThrow, map, or another explicit branch. It is usually a poor fit for fields, collection elements, and required parameters.
Q03Why should a repository return Optional<Task> instead of null for an expected missing task?Show answer
Optional makes absence explicit in the method signature and forces the caller to choose a missing-case policy, such as orElseThrow or a fallback. It does not turn the result into an HTTP response; that translation belongs to a later application boundary.
Production Pitfalls
Critical Anti-Patterns
- Pitfall: Comparing wrapper objects with == — == compares references for objects, so two Longs holding the same value can compare unequal. Small values appear to work because of Integer caching between -128 and 127, then fail in production with realistic numbers. Always compare objects with .equals().
- Pitfall: Catching exceptions silently — An empty catch block hides the failure from logs and monitoring while the system continues in a broken state. Catch narrowly, log or rethrow, and let the HTTP layer decide what the client sees.
- Pitfall: Memorizing Spring annotations before understanding plain Java — Annotations are metadata read by the framework; they automate construction and wiring but never replace language semantics. A developer who cannot read constructors, collections, and try/catch by hand cannot debug what the annotations generate.
Primary Documentation & Specifications
Lesson Primary References
Supplementary Module Resources