Java Atlas for Full-Stack Engineers

Fundamentals

Primitive Types vs Wrapper Types

Primitive types store raw values, while wrapper types are objects that can be null and are required by generics/collections.

1
2
3
4
int age = 18;
Integer nullableAge = null;

List<Integer> numbers = List.of(1, 2, 3);

== vs equals()

== compares primitive values or object references, while equals() compares object content when the class implements it accordingly.

1
2
3
4
5
String a = new String("hello");
String b = new String("hello");

a == b; // false
a.equals(b); // true

hashCode()

hashCode() returns a hash value used by hash-based collections such as HashMap; objects that are equal according to equals() must return the same hash code.

1
2
3
4
5
String a = "hello";
String b = "hello";

a.equals(b); // true
a.hashCode() == b.hashCode(); // true

String Immutability

Java String objects cannot be changed after creation; operations create a new string instead.

1
2
3
4
String a = "hello";
a.toUpperCase();

System.out.println(a); // "hello"

Exception Model

Java has checked exceptions, which must be caught or declared with throws, and unchecked exceptions, which may fail at runtime without explicit handling.

Checked exceptions represent expected external failures and must be handled or declared; unchecked exceptions usually indicate programming errors and may fail at runtime.

1
2
3
4
5
6
void readFile() throws IOException {
Files.readString(Path.of("test.txt")); // checked exception
}

String s = null;
s.length(); // unchecked: NullPointerException

Generics

Generics let classes and methods work with different types while keeping compile-time type safety.

1
2
3
List<String> names = new ArrayList<>();
names.add("Alice");
// names.add(123); // compile error

Optional

Optional<T> represents a value that may or may not exist, instead of directly using null.

1
2
3
Optional<String> name = Optional.of("Alice");

String value = name.orElse("Unknown");

Annotations

Annotations add metadata to classes, methods, or fields, and are heavily used by frameworks such as Spring to configure behavior.

1
2
3
@RestController
public class UserController {
}

record

A record is a concise way to define an immutable data carrier; Java automatically generates the constructor, accessors, equals(), hashCode(), and toString().

1
2
3
record User(String name, int age) {}

User user = new User("Alice", 25);

List, Set, Map

List stores ordered values, Set stores unique values, and Map stores key-value pairs.

1
2
3
List<String> names = List.of("Alice", "Bob");
Set<String> tags = Set.of("java", "spring");
Map<String, Integer> ages = Map.of("Alice", 25);

ArrayList

ArrayList is the most common List implementation and is backed by a dynamically resizable array.

1
2
3
List<String> names = new ArrayList<>();
names.add("Alice");
names.add("Bob");

Compared with List:

  • List is the interface.
  • ArrayList is a concrete implementation.

HashMap

HashMap is the most common Map implementation for key-value storage and is not thread-safe.

1
2
3
Map<String, Integer> ages = new HashMap<>();
ages.put("Alice", 25);
ages.get("Alice");

Compared with Map:

  • Map is the interface.
  • HashMap is a concrete implementation.

ConcurrentHashMap

ConcurrentHashMap is a thread-safe Map implementation designed for concurrent access.

1
2
Map<String, Integer> ages = new ConcurrentHashMap<>();
ages.put("Alice", 25);

Compared with HashMap:

  • HashMap is not thread-safe.
  • ConcurrentHashMap safely supports concurrent access.
  • Compound operations can still have race conditions.

Stream API

The Stream API provides a functional, chainable way to transform and process collections, similar to JavaScript’s filter, map, and reduce.

1
2
3
4
List<String> names = users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();

Lambda Expressions

Java lambdas are anonymous functions, similar to JavaScript arrow functions.

1
2
3
4
// Java
users.stream()
.filter(user -> user.isActive())
.toList();
1
2
// JavaScript
users.filter(user => user.isActive);

Functional Interfaces

A functional interface gives a lambda a concrete Java type. It has exactly one abstract method, and the lambda provides that method’s implementation.

1
2
3
4
5
6
7
@FunctionalInterface
interface Calculator {
int calculate(int a, int b);
}

Calculator add = (a, b) -> a + b;
add.calculate(1, 2);

In practice, Java often uses built-in functional interfaces such as Function, Predicate, and Consumer.

Method References

Method references are shorthand for lambdas that only call an existing method.

1
2
3
4
users.stream()
.filter(User::isActive)
.map(User::getName)
.toList();

User::isActive is equivalent to user -> user.isActive().

Java Runtime and Build System

JDK, JRE, and JVM

JDK provides Java development tools, JRE provides the runtime environment, and JVM executes compiled Java bytecode.

1
.java → javac → .class bytecode → JVM

JDK = JRE + development tools, while JRE = JVM + runtime libraries.

Java Bytecode and javac

javac compiles Java source code into platform-independent bytecode, which is stored in .class files and executed by the JVM.

1
2
3
4
5
Hello.java
↓ javac
Hello.class
↓ JVM
runs on the target system

Java Build and Runtime Flow

Java source code is compiled into bytecode, then executed by the JVM.

1
2
3
4
5
Main.java
↓ javac
Main.class
↓ java / JVM
program runs

Example source:

1
2
3
4
5
6
7
package com.example;

public class Main {
public static void main(String[] args) {
System.out.println("Hello");
}
}

The package normally matches the directory:

1
src/main/java/com/example/Main.java

After compilation:

1
target/classes/com/example/Main.class

A .class file contains the compiled bytecode for one Java class.

To run it:

1
java -cp target/classes com.example.Main

Here:

1
-cp target/classes

sets the classpath, which tells Java where to look for compiled classes.

Java resolves:

1
2
3
com.example.Main
↓
target/classes/com/example/Main.class

A JAR packages many .class files and resources into one distributable file.

1
2
3
4
5
.java source
↓ javac
.class files
↓ package
app.jar

Example:

1
2
3
4
app.jar
├── com/example/Main.class
├── com/example/user/User.class
└── application.properties

A normal JAR can be used as a classpath entry:

1
java -cp app.jar com.example.Main

Spring Boot usually creates an executable JAR that can run directly:

1
java -jar app.jar

In a Maven project, Maven usually handles compilation, classpath, dependencies, and packaging for you:

1
2
./mvnw package
./mvnw spring-boot:run

Maven

Maven is a build and dependency management tool for Java projects. It handles tasks such as compilation, testing, packaging, and running plugins.

1
2
3
./mvnw compile
./mvnw test
./mvnw package

pom.xml

pom.xml is Maven’s project configuration file. It defines project metadata, dependencies, Java version, plugins, and build settings.

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

Dependency Management

Maven downloads declared dependencies and their transitive dependencies from repositories, then adds them to the project’s classpath automatically.

1
2
3
4
5
6
7
pom.xml
↓
Maven resolves dependencies
↓
downloads JARs
↓
adds them to the classpath

Maven Wrapper

mvnw is a project-local wrapper around Maven that downloads and uses the Maven version configured by the project.

1
./mvnw package

This avoids requiring every developer to install the same Maven version globally.

Spring Boot

Spring Boot Application Structure

A typical Spring Boot application is organized into layers such as controller, service, and repository.

1
2
3
4
5
6
7
8
src/main/java/com/example/app/
├── Application.java
├── controller/
├── service/
├── repository/
├── entity/
├── dto/
└── config/
  • Application.java: application entry point.
  • controller: handles HTTP requests and responses.
  • service: contains business logic.
  • repository: handles database access.
  • entity: represents persistent database data.
  • dto: defines API request/response data shapes.
  • config: contains application and framework configuration.

Typical request flow:

1
2
3
4
5
6
7
8
9
10
11
HTTP Request
↓
Controller
↓
DTO
↓
Service
↓
Repository
↓
Entity / Database

DTO, Entity, and Repository have different responsibilities:

1
2
3
DTO        = API data shape
Entity = database data model
Repository = database access layer

Application Startup

A Spring Boot application starts from a standard Java main() method and boots the Spring application context.

1
2
3
4
5
6
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
1
2
3
4
5
6
7
main()
→ SpringApplication.run()
→ create application context
→ scan and create beans
→ apply auto-configuration
→ start embedded web server
→ application ready

IoC Container

The Spring IoC Container creates and manages application objects (beans) instead of requiring the application to instantiate them manually.

1
2
3
4
Spring IoC Container
├── UserController
├── UserService
└── UserRepository

Dependency Injection

Dependency Injection means Spring provides an object’s dependencies instead of the object creating them itself.

1
2
3
public UserController(UserService userService) {
this.userService = userService;
}

Instead of:

1
UserService userService = new UserService();

Spring Beans

A Spring Bean is a Java object created, configured, and managed by the Spring IoC Container.

1
2
3
@Service
public class UserService {
}

UserService becomes a Spring-managed bean.

Component Scanning

Spring scans configured packages for annotated classes and automatically registers them as beans.

1
2
3
4
com.example.app
├── controller
├── service
└── repository

Classes under the application’s base package are typically scanned automatically.

@Component

@Component marks a general-purpose class as a Spring-managed bean.

1
2
3
@Component
public class EmailClient {
}

@Service

@Service is a specialized @Component used for business logic.

1
2
3
@Service
public class UserService {
}

@Repository

@Repository is a specialized @Component used for data access.

1
2
3
@Repository
public class UserRepository {
}

@Configuration

@Configuration marks a class that defines Spring configuration and beans.

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {

@Bean
public EmailClient emailClient() {
return new EmailClient();
}
}

application.yml

application.yml stores application configuration such as ports, database settings, and environment-specific values.

1
2
3
4
5
6
server:
port: 8080

spring:
datasource:
url: jdbc:h2:mem:testdb

Profiles

Profiles allow different configurations for environments such as development, testing, and production.

1
2
3
spring:
profiles:
active: dev

Common profiles:

1
2
3
dev
test
prod

Starters

Spring Boot starters are dependency bundles for common features.

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

For example, spring-boot-starter-web brings in the dependencies needed for building web applications.

Auto-Configuration

Spring Boot automatically configures common components based on the dependencies and configuration found in the project.

1
2
3
4
spring-boot-starter-web
→ Spring detects web dependencies
→ configures Spring MVC
→ configures embedded web server

This reduces the amount of manual framework configuration required.

Spring Web / MVC

  • Controllers: receive HTTP requests and return responses.
  • Request Mapping: maps HTTP methods and paths to controller methods.
  • Path Variables: read dynamic values from the URL path, e.g. /users/{id}.
  • Query Parameters: read values from the query string, e.g. /users?role=admin.
  • Request Bodies: convert request JSON into Java objects with @RequestBody.
  • DTOs: define API request/response data shapes.
  • Validation: validate incoming DTOs with annotations such as @NotBlank, @Email, and @Valid.
  • Response Handling: return objects directly or use ResponseEntity for explicit status/header control.
  • Global Exception Handling: handle controller exceptions centrally with @RestControllerAdvice.
  • Jackson: converts JSON ↔ Java objects automatically.
  • Filters: run around requests at the servlet level, before Spring MVC controller handling.
  • Interceptors: run inside Spring MVC before/after controller execution.
  • Request Lifecycle:
id
1
2
3
4
5
6
7
8
9
10
HTTP Request
→ Filter
→ DispatcherServlet
→ Interceptor
→ Controller
→ Service
→ Repository
→ Controller Response
→ Jackson
→ HTTP Response

Typical controller:

id
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
@RequestMapping("/users")
public class UserController {

@GetMapping("/{id}")
public UserDto getUser(@PathVariable Long id) {
return userService.getUser(id);
}

@PostMapping
public UserDto createUser(
@Valid @RequestBody CreateUserRequest request
) {
return userService.createUser(request);
}
}

JPA and Hibernate

ORM Basics

ORM (Object-Relational Mapping) maps Java objects to relational database tables.

1
2
3
4
5
Java Object
↓
ORM
↓
Database Row

Example:

1
2
3
4
5
6
7
@Entity
public class User {
@Id
private Long id;

private String name;
}

This User entity can be mapped to a database table such as users.

JPA vs Hibernate

JPA stands for Java Persistence API. It is a standard specification for ORM in Java.

Hibernate is one of the most common JPA implementations.

1
2
3
4
5
6
7
8
9
Your Code
↓
JPA API
↓
Hibernate
↓
SQL
↓
Database

A useful mental model:

1
2
JPA       = specification / interface
Hibernate = implementation

Entities

An entity is a Java class that represents persistent data stored in the database.

1
2
3
4
5
6
7
8
9
@Entity
public class User {

@Id
private Long id;

private String name;
private String email;
}

The entity defines the persistence model, while actual data exists as Java objects and database rows.

1
2
User user = new User();
user.setName("Alice");

Primary Keys

@Id marks the primary key of an entity.

1
2
3
@Id
@GeneratedValue
private Long id;

@GeneratedValue tells JPA that the ID should usually be generated automatically.

Repositories

Repositories provide the data access layer for entities.

1
2
3
public interface UserRepository
extends JpaRepository<User, Long> {
}

The generic types mean:

1
2
User = entity type
Long = primary key type

You can then use:

1
2
3
4
userRepository.save(user);
userRepository.findById(1L);
userRepository.findAll();
userRepository.deleteById(1L);

Spring Data JPA

Spring Data JPA is a Spring abstraction built on top of JPA.

It automatically provides common CRUD and query operations, so you do not need to write most repository boilerplate manually.

1
2
3
4
5
6
7
8
9
Application Code
↓
Spring Data JPA
↓
JPA
↓
Hibernate
↓
Database

Entity Relationships

JPA can map relationships between entities and database tables.

Common relationship types:

1
2
3
@OneToOne
@OneToMany
@ManyToOne

One-to-One

One entity is associated with exactly one other entity.

1
2
@OneToOne
private Profile profile;

Example:

1
User 1 ─── 1 Profile

One-to-Many

One entity is associated with many child entities.

1
2
@OneToMany
private List<Task> tasks;

Example:

1
User 1 ─── N Task

Many-to-One

Many entities reference the same parent entity.

1
2
@ManyToOne
private User user;

Example:

1
Task N ─── 1 User

One-to-Many and Many-to-One are often two sides of the same database relationship.

Lazy Loading

Lazy loading delays loading related data until it is actually accessed.

1
User user = userRepository.findById(1L).orElseThrow();

Hibernate may initially load only the user:

1
SELECT * FROM users WHERE id = 1;

Then when:

1
user.getTasks();

is accessed, Hibernate may issue another query:

1
SELECT * FROM tasks WHERE user_id = 1;

Lazy loading avoids loading unnecessary related data, but can cause extra queries.

Eager Loading

Eager loading loads related data immediately with the parent entity.

1
2
3
load User
+
load related Tasks immediately

This can be convenient, but may load more data than needed.

N+1 Query Problem

The N+1 problem happens when one query loads a list of entities, then one additional query is executed for each entity’s related data.

Example:

1
2
3
4
5
List<User> users = userRepository.findAll();

for (User user : users) {
user.getTasks();
}

Possible query pattern:

1
2
1 query  → load all users
N queries → load tasks for each user

For 100 users:

1
1 + 100 = 101 queries

This can cause serious performance problems.

Persistence Context

The persistence context is Hibernate’s managed environment for entity objects during a session or transaction.

When an entity is loaded:

1
User user = userRepository.findById(1L).orElseThrow();

Hibernate keeps track of that object.

Conceptually:

1
2
3
4
5
6
7
Database Row
↓
Hibernate
↓
Managed User Entity
↓
Persistence Context

The same entity loaded again within the same context may reuse the already managed object instead of creating another independent copy.

Dirty Checking

Dirty checking means Hibernate automatically detects changes to managed entities.

1
2
3
4
5
6
@Transactional
public void renameUser(Long id) {
User user = userRepository.findById(id).orElseThrow();

user.setName("Bob");
}

Even without calling:

1
userRepository.save(user);

Hibernate can detect that the managed entity changed and generate an update when the transaction is committed.

1
2
3
UPDATE users
SET name = 'Bob'
WHERE id = ?;

Typical persistence flow:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Controller
↓
Service
↓
Repository
↓
Spring Data JPA
↓
JPA
↓
Hibernate
↓
SQL
↓
Database

And when reading data:

1
2
3
4
5
6
7
8
9
10
11
Database Row
↓
Hibernate
↓
Entity
↓
Service
↓
DTO
↓
HTTP Response

Transactions

Transaction Basics

A transaction groups multiple database operations into one unit of work.

1
2
3
4
5
all succeed
→ commit

any critical step fails
→ rollback

Example:

1
2
3
create order
→ deduct inventory
→ save payment record

These operations should usually succeed or fail together.

@Transactional

@Transactional tells Spring to execute a method inside a database transaction.

1
2
3
4
5
@Transactional
public void createOrder() {
orderRepository.save(order);
inventoryService.deductStock();
}

Spring typically opens a transaction before the method runs and commits it when the method finishes successfully.

Commit and Rollback

A successful transaction is committed to the database.

1
2
method succeeds
→ COMMIT

If the transaction fails, changes can be rolled back.

1
2
method throws exception
→ ROLLBACK

Transaction Boundaries

The transaction boundary defines where a transaction starts and ends.

1
2
3
4
5
6
7
8
9
@Transactional
public void createOrder() {
// transaction starts before this method

saveOrder();
deductStock();

// transaction commits after the method succeeds
}

Transactions are commonly placed at the service layer because one business operation may involve multiple repository calls.

Transaction Propagation

Propagation defines what happens when one transactional method calls another transactional method.

The default behavior is usually:

1
2
3
4
5
existing transaction
→ reuse it

no existing transaction
→ create one

This is Propagation.REQUIRED.

1
2
3
4
@Transactional
public void createOrder() {
paymentService.createPayment();
}

Both operations can participate in the same transaction.

Rollback Rules

By default, Spring normally rolls back for unchecked exceptions such as RuntimeException.

1
2
3
4
5
6
@Transactional
public void createOrder() {
orderRepository.save(order);

throw new RuntimeException("failed");
}

The saved order is rolled back.

Checked exceptions may require explicit configuration:

1
@Transactional(rollbackFor = Exception.class)

Common @Transactional Pitfalls

A common pitfall is calling a transactional method from another method in the same class.

1
2
3
4
5
6
7
public void create() {
save(); // may bypass Spring proxy behavior
}

@Transactional
public void save() {
}

Spring transactions are usually implemented with proxies, so self-invocation can bypass the transactional proxy.

Another common mistake is catching an exception and hiding it:

1
2
3
4
5
6
7
8
@Transactional
public void createOrder() {
try {
saveOrder();
} catch (Exception e) {
// exception swallowed
}
}

Spring may think the method succeeded and commit the transaction.

Database Connections and Transactions

A database transaction normally operates through a database connection.

Conceptually:

1
2
3
4
5
6
Spring
→ obtain DB connection
→ BEGIN transaction
→ execute SQL
→ COMMIT / ROLLBACK
→ return connection to pool

In Spring Boot, connections are commonly obtained from a connection pool such as HikariCP.

Typical flow:

1
2
3
4
5
Controller
→ Service @Transactional
→ Repository
→ SQL
→ Database

The service method usually defines the business transaction boundary.

Spring Security Basics

Authentication

Authentication verifies who the user is.

Example:

1
2
3
email + password
→ verify credentials
→ authenticated user

Authorization

Authorization decides what an authenticated user is allowed to do.

1
2
USER  → read profile
ADMIN → manage users

Security Filter Chain

Spring Security processes requests through a chain of security filters before they reach controllers.

1
2
3
4
HTTP Request
→ Security Filters
→ Authentication / Authorization
→ Controller

SecurityContext

SecurityContext stores information about the currently authenticated user during the request.

1
2
3
4
SecurityContext
└── Authentication
├── user
└── roles / authorities

Password Hashing

Passwords should not be stored directly. Spring commonly uses PasswordEncoder, such as BCrypt, to store password hashes.

1
passwordEncoder.encode("password");
1
2
3
password
→ hash
→ database

JWT Authentication

JWT is commonly used for stateless API authentication.

1
2
3
4
5
6
7
8
Login
→ verify credentials
→ issue JWT

Request + JWT
→ verify token
→ identify user
→ authorize request

Usually the token is sent as:

1
Authorization: Bearer <token>

Method-Level Security

Authorization rules can also be applied directly to service or controller methods.

1
2
3
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
}

CORS

CORS controls whether a browser frontend from another origin is allowed to call the API.

1
2
frontend: http://localhost:3000
backend: http://localhost:8080

Spring must allow the frontend origin when cross-origin requests are needed.

CSRF

CSRF is an attack where a browser is tricked into sending an authenticated request without the user’s intention.

It mainly matters when authentication relies on automatically sent credentials such as cookies.

For stateless APIs using JWTs in the Authorization header, CSRF protection is often configured differently or disabled depending on the architecture.

Typical Flow

1
2
3
4
5
6
HTTP Request
→ Security Filter Chain
→ authenticate user
→ store Authentication in SecurityContext
→ check authorization
→ Controller

Concurrency Basics

Threads

A thread is an independent execution path inside a process. A Spring Boot application can handle multiple requests concurrently using multiple threads.

1
2
3
Request A → Thread 1
Request B → Thread 2
Request C → Thread 3

Thread Safety

Code is thread-safe when multiple threads can use it concurrently without corrupting shared state or producing incorrect results.

Race Conditions

A race condition happens when multiple threads read and modify the same shared state at the same time.

1
2
3
4
5
6
7
class Counter {
private int count = 0;

public void increment() {
count++; // not atomic
}
}

Two threads may both read the same old value:

1
2
3
4
Thread A reads 0
Thread B reads 0
Thread A writes 1
Thread B writes 1

The expected result is 2, but the actual result may be 1.

synchronized

synchronized allows only one thread at a time to execute a protected method or block for the same lock.

1
2
3
4
5
6
7
class Counter {
private int count = 0;

public synchronized void increment() {
count++;
}
}

This prevents multiple threads from modifying the protected state at the same time.

Atomic Types

For simple atomic operations such as counters, Java provides classes such as AtomicInteger.

1
2
3
AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();

This is often simpler than manually using synchronized.

volatile

volatile guarantees visibility: when one thread updates a variable, other threads can see the latest value.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Worker {
private volatile boolean running = true;

public void stop() {
running = false;
}

public void run() {
while (running) {
// work
}
}
}

volatile does not make compound operations atomic.

1
2
3
private volatile int count = 0;

count++; // still not thread-safe
1
2
3
volatile     → visibility
synchronized → mutual exclusion + visibility
Atomic types → atomic operations

Thread Pools

Creating a new thread for every task is expensive. A thread pool keeps reusable worker threads and assigns tasks to them.

1
2
3
4
5
6
7
8
Tasks
↓
Task Queue
↓
Thread Pool
├── Worker 1
├── Worker 2
└── Worker 3

ExecutorService

ExecutorService is Java’s high-level API for submitting work to a thread pool.

1
2
3
4
ExecutorService executor =
Executors.newFixedThreadPool(4);

executor.submit(() -> doWork());

Instead of manually creating:

1
new Thread(() -> doWork()).start();

the executor manages and reuses worker threads.

CompletableFuture

CompletableFuture represents an asynchronous result and is conceptually similar to a JavaScript Promise.

1
2
3
4
CompletableFuture
.supplyAsync(() -> fetchUser())
.thenApply(User::getName)
.thenAccept(System.out::println);

Rough JavaScript equivalent:

1
2
3
fetchUser()
.then(user => user.name)
.then(name => console.log(name));

Common mappings:

1
2
3
4
5
6
JavaScript Promise       Java CompletableFuture

.then(...) ≈ thenApply(...)
side-effect .then ≈ thenAccept(...)
Promise.all(...) ≈ CompletableFuture.allOf(...)
.catch(...) ≈ exceptionally(...)

Java asynchronous work is commonly executed using threads or thread pools, while JavaScript typically uses an event-loop-based model.

Shared Mutable State

Shared mutable state is data that:

  1. can be changed, and
  2. can be accessed by multiple threads.
1
private int count = 0;

Shared mutable state is one of the main sources of concurrency bugs.

Prefer immutable data or method-local variables when possible.

Singleton Bean Thread Safety

Spring beans are singleton by default, so multiple HTTP requests may use the same bean instance concurrently.

1
2
3
4
5
6
7
8
9
@Service
public class UserService {

private int requestCount = 0;

public void handleRequest() {
requestCount++; // race condition
}
}

Conceptually:

1
2
3
Request A ─┐
├→ same UserService instance
Request B ─┘

A better default is to keep Spring services stateless:

1
2
3
4
5
6
7
8
@Service
public class UserService {

public void handleRequest() {
int localValue = 0;
// local to this invocation
}
}

If shared mutable state is actually required, use the appropriate synchronization mechanism or move the state to a system designed for shared data, such as a database or Redis.

JVM Basics

JVM Memory Model Overview

At runtime, the JVM manages several memory areas.

1
2
3
4
5
JVM Memory
├── Heap
├── Thread Stacks
├── Metaspace
└── Native / JVM internal memory

Stack vs Heap

Each thread has its own stack for method calls and local variables. The heap stores objects shared across the application.

1
User user = new User("Alice");
1
2
3
4
5
Stack
└── user reference
↓
Heap
└── User("Alice")

Object Allocation

Objects created with new are generally allocated on the heap.

1
User user = new User();

The local variable stores a reference to the object rather than the object itself.

Garbage Collection

Garbage Collection automatically reclaims heap memory from objects that are no longer reachable.

1
2
User user = new User();
user = null;

The old User object becomes eligible for garbage collection once nothing reachable refers to it.

Class Loading

Compiled .class files must be loaded into the JVM before they can be used.

1
2
3
4
5
.java
↓ javac
.class
↓ ClassLoader
JVM runtime class

Class loaders locate classes from classpath entries such as directories and JAR files.

JVM Memory Settings

Heap size can be configured when starting the JVM.

1
java -Xms512m -Xmx2g -jar app.jar
1
2
-Xms → initial heap size
-Xmx → maximum heap size

Thread Dumps

A thread dump is a snapshot of what all JVM threads are doing at a specific moment.

It is useful for diagnosing:

1
2
3
4
deadlocks
blocked threads
high CPU
hung requests

Heap Dumps

A heap dump is a snapshot file containing the objects currently stored in JVM heap memory and their references.

1
heapdump.hprof

It is useful for diagnosing:

1
2
3
memory leaks
OutOfMemoryError
unexpected memory usage

A heap dump can be generated with JVM tools such as jcmd:

1
jcmd <pid> GC.heap_dump heapdump.hprof

The JVM can also generate one automatically on OOM:

1
2
3
4
java \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=heapdump.hprof \
-jar app.jar

Caching & Redis

Why Caching

Caching stores frequently accessed data in faster storage to reduce latency and database load.

1
2
3
4
Client
→ Application
→ Redis Cache
→ Database

Typical flow:

1
2
cache hit  → return cached data
cache miss → query database → write cache → return data

Redis Basics

Redis is an in-memory data store commonly used for caching, sessions, counters, queues, rate limiting, and distributed coordination.

1
2
3
Application
→ Redis
→ fast in-memory read/write

In distributed deployments, Redis usually runs as a shared service that multiple application instances access over the network.

1
2
3
Machine A / App A ─┐
Machine B / App B ─┼→ Shared Redis
Machine C / App C ─┘

This shared access allows different machines and processes to coordinate through the same Redis state.

Common Data Types

Common Redis data structures include:

1
2
3
4
5
String      → simple values / counters
Hash → object-like key-value fields
List → ordered values / queues
Set → unique values
Sorted Set → ranked values

Cache-Aside Pattern

A common caching pattern is:

1
2
3
4
5
6
read request
→ check Redis
→ cache hit: return
→ cache miss: query DB
→ store result in Redis
→ return

The application controls when data enters and leaves the cache.

TTL (Time To Live) / Expiration

Cached values usually have an expiration time.

1
2
user:123
TTL = 5 minutes

After the TTL expires, Redis automatically removes the key.

Cache Invalidation

When database data changes, the corresponding cache may become stale.

Typical approach:

1
2
3
4
update database
→ delete cached value
→ next read reloads fresh data
→ store fresh value in Redis

The database normally remains the source of truth.

Cache Penetration / Stampede / Avalanche

Cache penetration: repeated requests for data that does not exist reach the database every time.

1
2
3
Redis miss
→ Database miss
→ repeat

Cache stampede: a hot key expires and many requests query the database simultaneously.

1
2
3
hot key expires
→ many cache misses
→ many DB queries

Cache avalanche: many cached keys expire around the same time, causing a large database traffic spike.

Common protections include:

1
2
3
4
short-term caching of missing values
randomized TTLs
locking / request coalescing
rate limiting

Distributed Locks

A distributed lock ensures that only one application instance performs a shared operation at a time.

A local Java lock such as synchronized only works inside one JVM:

1
2
Machine A → JVM A → local lock
Machine B → JVM B → separate local lock

It cannot coordinate multiple machines.

Redis can provide a lock that all application instances can see:

1
2
3
Machine A / App A ─┐
Machine B / App B ─┼→ Redis Lock → Shared Resource
Machine C / App C ─┘

Conceptually:

1
SET lock:report instance-A NX EX 30
1
2
NX    → create only if the lock does not already exist
EX 30 → automatically expire after 30 seconds

If App A acquires the lock:

1
2
3
App A → executes task
App B → lock already exists
App C → lock already exists

Expiration helps prevent the lock from remaining forever if the owner crashes.

Production applications commonly use mature libraries such as Redisson instead of implementing distributed locking manually.

Redis vs Database

Redis is usually not a replacement for the primary database.

1
2
Database → durable source of truth
Redis → fast cache / coordination layer

Messaging & Kafka

Message Queue Basics

Messaging decouples services by allowing work to be processed asynchronously.

1
2
3
Producer
→ Message Broker
→ Consumer

Instead of:

1
2
3
Service A
→ waits for Service B
→ waits for response

the producer can publish a message and continue.

Kafka Basics

Kafka is a distributed event-streaming platform commonly used for asynchronous communication, event-driven systems, and high-throughput data processing.

1
2
3
Producer
→ Kafka Topic
→ Consumer

Producer / Consumer

A producer publishes messages.

A consumer reads and processes messages.

1
2
3
4
Order Service
→ "OrderCreated"
→ Kafka
→ Email Service

Topic

A topic is a named stream of messages.

1
2
3
orders
payments
user-events

Producers write to topics and consumers subscribe to them.

Partitions

A Kafka topic is split into partitions for scalability and parallel processing.

1
2
3
4
orders
├── Partition 0
├── Partition 1
└── Partition 2

Each partition is an ordered log.

Kafka guarantees ordering within a partition, not across the entire topic.

When a message has a key, the producer typically uses that key to determine the partition.

Conceptually:

1
partition = hash(key) % numberOfPartitions

For example, if orderId is used as the key:

1
2
3
4
5
OrderCreated      key=1001
PaymentStarted key=1001
PaymentCompleted key=1001
↓
same partition

Messages with the same key normally go to the same partition, which preserves ordering for that key.

Without a key, messages can be distributed across partitions for better load balancing and throughput.

1
2
same key       → same partition → ordering
more partitions → more parallelism

Consumer Groups

Consumers in the same consumer group share the work.

1
2
3
4
Topic
├── Partition 0 → Consumer A
├── Partition 1 → Consumer B
└── Partition 2 → Consumer C

Within one consumer group, a partition is assigned to only one consumer at a time.

Different consumer groups can independently consume the same topic.

Offset

An offset represents a message’s position inside a partition.

1
2
3
offset 0 → message A
offset 1 → message B
offset 2 → message C

Consumers track offsets to know how far they have processed.

1
2
3
processed through offset 2
→ restart
→ continue from the next position

At-Least-Once Delivery

A common Kafka delivery model is at least once, meaning a message may occasionally be processed more than once.

For example:

1
2
3
4
consumer processes message
→ crashes before offset is committed
→ restarts
→ message is processed again

Therefore consumers should often be idempotent.

Idempotency

An operation is idempotent if processing the same event multiple times does not create incorrect duplicate effects.

1
2
3
4
5
6
OrderCreated #123
→ processed once

duplicate OrderCreated #123
→ detected as already processed
→ safely ignored

Retry / Dead Letter Handling

Failed messages may be retried.

1
2
3
4
5
6
7
message
→ process
→ fail
→ retry
→ retry
→ still fail
→ dead-letter topic / queue

A dead-letter topic stores repeatedly failing messages for later inspection or reprocessing.

Kafka vs REST

REST is synchronous and request/response oriented:

1
2
3
4
Service A
→ HTTP request
→ Service B
→ response

Kafka is asynchronous and event oriented:

1
2
3
4
Service A
→ event
→ Kafka
→ Service B processes later

Use REST when an immediate response is needed.

Use Kafka when asynchronous processing, loose coupling, event distribution, or high throughput is useful.


Production Infrastructure

Load Balancer

A load balancer distributes incoming traffic across multiple application instances.

1
2
3
4
5
6
Clients
↓
Load Balancer
├── App 1
├── App 2
└── App 3

This enables horizontal scaling and improves availability.


API Gateway

An API gateway provides a centralized entry point for backend APIs.

1
2
3
4
5
Client
→ API Gateway
├── User Service
├── Order Service
└── Payment Service

Common responsibilities include:

1
2
3
4
5
6
routing
authentication
rate limiting
logging
CORS
request transformation

A load balancer mainly distributes traffic, while an API gateway applies API-level routing and policies.


Nginx

Nginx is a high-performance web server and reverse proxy commonly placed in front of backend applications.

1
2
3
Client
→ Nginx
→ Spring Boot

Common uses include:

1
2
3
4
5
6
reverse proxy
TLS(Transport Layer Security) termination
static file serving
load balancing
compression
caching

Reverse Proxy

A reverse proxy receives client requests and forwards them to internal backend services.

1
2
3
4
Client
→ https://example.com/api/users
→ Nginx
→ http://app:8080/users

Example:

1
2
3
4
5
6
7
server {
listen 80;

location /api/ {
proxy_pass http://app:8080/;
}
}

The client only communicates with Nginx and does not need to know the backend server’s internal address.

Static Files

Nginx can also serve frontend static assets.

1
2
3
4
Browser
→ Nginx
├── frontend files
└── /api/* → Spring Boot

Load Balancing

Nginx can distribute requests across multiple backend instances.

1
2
3
4
5
6
Client
↓
Nginx
├── App 1
├── App 2
└── App 3

TLS Termination

Nginx can handle HTTPS while backend services communicate through an internal network.

1
2
3
4
5
Client
→ HTTPS
→ Nginx
→ HTTP
→ Spring Boot

Docker

Docker packages an application and its runtime dependencies into an isolated container so it can run consistently across environments.

1
2
3
4
Spring Boot Application
→ JAR
→ Docker Image
→ Container

Example:

1
2
3
4
5
FROM eclipse-temurin:21-jre

COPY target/app.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]

Build and run:

1
2
docker build -t my-app .
docker run -p 8080:8080 my-app

Dockerfile

A Dockerfile contains instructions for building a Docker image.

1
2
3
Dockerfile
→ docker build
→ Docker Image

Image vs Container

A Docker image is an immutable application package.

A container is a running instance of an image.

1
2
3
4
5
6
Docker Image
├── Java runtime
├── app.jar
└── dependencies
↓
Container

The same image can run as multiple containers.

1
2
3
4
my-app image
├── Container A
├── Container B
└── Container C

Registry

A container registry stores and distributes Docker images.

1
2
3
4
Developer / CI
→ build image
→ push to Registry
→ production pulls image

Examples include Docker Hub, Amazon ECR, and Google Artifact Registry.

Docker Compose

Docker Compose allows multiple containers to be defined and started together.

1
2
3
4
5
6
7
8
9
10
11
services:
app:
build: .
ports:
- "8080:8080"

redis:
image: redis

postgres:
image: postgres
1
2
3
4
Docker Compose
├── Spring Boot
├── Redis
└── PostgreSQL

This is commonly useful for local development.


Kubernetes

Kubernetes (K8s) orchestrates containers across multiple machines.

Docker runs containers, while Kubernetes manages containers at scale.

1
2
3
4
5
6
7
8
9
Docker
→ package and run containers

Kubernetes
→ deploy
→ scale
→ restart
→ network
→ manage containers

A Kubernetes cluster contains multiple machines called nodes.

1
2
3
4
5
6
Kubernetes Cluster
├── Node A
│ ├── Pod
│ └── Pod
└── Node B
└── Pod

Pod

A Pod is the smallest deployable unit in Kubernetes.

A Pod commonly contains one application container.

1
2
Pod
└── Spring Boot Container

Multiple Pods can run replicas of the same application.

1
2
3
4
Spring Boot
├── Pod 1
├── Pod 2
└── Pod 3

Deployment

A Deployment manages application Pods and their desired replica count.

1
2
3
4
5
apiVersion: apps/v1
kind: Deployment

spec:
replicas: 3

Kubernetes continuously tries to maintain the desired state.

1
2
3
4
5
desired replicas = 3

Pod crashes
→ Kubernetes detects failure
→ creates replacement Pod

Service

Pods can be created and destroyed, so their IP addresses are not stable.

A Kubernetes Service provides a stable network endpoint for a group of Pods.

1
2
3
4
5
Client
→ Kubernetes Service
├── Pod A
├── Pod B
└── Pod C

The Service can also distribute traffic between Pods.

ConfigMap and Secret

Configuration should normally be separated from the Docker image.

1
2
ConfigMap → normal configuration
Secret → passwords / tokens / credentials

This allows the same Docker image to run with different environment configurations.

Health Checks

Kubernetes can monitor application health.

1
2
3
4
5
Liveness Probe
→ should this container be restarted?

Readiness Probe
→ should this Pod receive traffic?

Spring Boot Actuator endpoints are commonly used for health checks.

Scaling

Kubernetes can increase or decrease the number of application replicas.

1
2
3
4
5
traffic increases
→ increase Pods

3 Pods
→ 10 Pods

This is horizontal scaling.


Rate Limiting

Rate limiting restricts how frequently a client can call an API.

1
100 requests / minute / user

It helps protect services from abuse and traffic spikes.

Common strategies include:

1
2
3
4
fixed window
sliding window
token bucket
leaky bucket

Redis is commonly used to store shared rate-limit state across multiple application instances.


Background Jobs

Long-running or non-urgent work can be moved out of the HTTP request path.

1
2
3
4
5
6
HTTP Request
→ create job
→ return response

Background Worker
→ process job later

Common examples include:

1
2
3
4
5
sending email
image processing
video processing
report generation
data cleanup

Background jobs can be implemented using job queues, message brokers, or scheduled workers.


Timeout and Retry

External calls should have a maximum waiting time.

1
2
3
4
Service A
→ Service B
→ no response
→ timeout

Without timeouts, slow dependencies can consume application threads and resources indefinitely.

Temporary failures can sometimes be retried.

1
2
3
4
request
→ fail
→ wait
→ retry

Retries should normally have a limit and use backoff.

1
2
3
retry 1 → 100ms
retry 2 → 200ms
retry 3 → 400ms

This is commonly called exponential backoff.

Uncontrolled retries can make an outage worse by creating additional traffic.


HTTP Idempotency

Idempotency prevents retries or duplicate requests from creating duplicate side effects.

For example:

1
2
POST /payments
Idempotency-Key: abc123

The first request:

1
2
3
abc123
→ process payment
→ save result

If the same request is sent again:

1
2
3
abc123 already processed
→ return previous result
→ do not charge again

Idempotency is especially important for operations such as:

1
2
3
4
5
payments
orders
webhooks
background jobs
retries

Observability

Observability helps understand what a production system is doing.

The three common signals are:

1
2
3
Logs    → what happened
Metrics → how much / how often
Traces → where time was spent across services

Common metrics include:

1
2
3
4
5
6
request rate
error rate
latency
CPU
memory
database connections

A distributed trace can show where a request became slow.

1
2
3
4
API Gateway        10ms
→ Order Service 30ms
→ Payment Service 800ms
→ Database 20ms

Typical debugging flow:

1
2
3
4
5
slow request
→ check metrics
→ inspect traces
→ inspect logs
→ identify database / service / network / thread bottleneck

Typical Production Deployment

A common production architecture may look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Internet
↓
DNS
↓
Load Balancer / Nginx / Ingress
↓
Kubernetes Service
↓
Spring Boot Pods
├── Pod 1
├── Pod 2
└── Pod 3
↓
├── Database
├── Redis
└── Kafka

A typical build and deployment flow is:

1
2
3
4
5
6
7
Java Source
→ Maven
→ JAR
→ Docker Image
→ Container Registry
→ Kubernetes Deployment
→ Pods

Key responsibilities:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Nginx
→ reverse proxy / web server / TLS / load balancing

Docker
→ package and run applications as containers

Kubernetes
→ deploy, scale, and manage containers

Redis
→ caching / shared state / distributed coordination

Kafka
→ asynchronous messaging and event streaming
Author

Chili

Posted on

2026-09-18

Updated on

2026-09-25

Licensed under

Comments