more text

This commit is contained in:
tarun-elango
2026-04-26 14:09:04 -04:00
parent 26810e43d0
commit be31df2d44
22 changed files with 10664 additions and 0 deletions
+608
View File
@@ -0,0 +1,608 @@
# File 1: Java Fundamentals
Java is often introduced as "write once, run anywhere," but that slogan only makes sense after you understand what Java is trying to optimize for: predictable behavior, strong tooling, portable runtime execution, and maintainability in medium-to-large systems. If you have seen Python or JavaScript first, Java can feel more explicit and more verbose. That is intentional. Java asks you to declare structure early so the compiler, IDE, runtime, and teammates can reason about your code reliably.
This file builds the base mental model you need before object-oriented design or concurrency starts to make sense. The goal is not just to memorize syntax. The goal is to understand what happens from source code to running program, why types matter, how control flow shapes logic, and how Java code is organized in real projects.
## Why Java Still Matters
Java has been around for decades, but it stays relevant because it solves real engineering problems well:
- teams need code that is easy to refactor safely
- backend systems need stable performance under load
- large organizations need strong tooling, dependency management, and observability support
- the JVM ecosystem provides mature libraries for networking, concurrency, persistence, messaging, and security
In production, Java commonly appears in:
- backend APIs handling payments, inventory, identity, notifications, and reporting
- internal enterprise systems with long maintenance lifetimes
- streaming and data-processing systems
- Android history, although modern Android development now leans heavily on Kotlin
- high-throughput services built with frameworks such as Spring Boot, Micronaut, Quarkus, or Dropwizard
Java is not popular because it is the shortest language to write. It is popular because it creates a strong balance between developer productivity, runtime performance, and operational predictability.
## The Java Ecosystem: JDK, JRE, and JVM
One of the first sources of confusion for beginners is that people casually say "install Java" when they actually mean different pieces of the platform.
### Intuition
Think of Java like a small software factory:
- you write source code
- a compiler turns it into bytecode
- a runtime executes that bytecode
- development tools help you debug, package, test, and ship the application
The three terms below refer to different layers in that process.
### JDK
The JDK, or Java Development Kit, is what you install when you want to build Java programs. It includes:
- the compiler `javac`
- the Java launcher `java`
- debugging and inspection tools like `jdb`, `jstack`, `jmap`, `jcmd`
- the runtime needed to execute programs
- standard libraries used by your code
If you are writing, compiling, packaging, or debugging Java, the JDK is your full toolkit.
### JRE
The JRE, or Java Runtime Environment, historically referred to the runtime needed to execute already-compiled Java applications. In older explanations, the distinction was:
- JDK = build and run
- JRE = run only
In modern Java distributions, the JRE is less emphasized as a separate installable concept. Still, the term matters because many articles and interviews use it. Conceptually, it means the runtime layer rather than the full developer toolkit.
### JVM
The JVM, or Java Virtual Machine, is the engine that actually runs Java bytecode. It handles:
- loading classes into memory
- verifying bytecode safety
- interpreting or JIT-compiling code into machine instructions
- memory allocation and garbage collection
- thread scheduling integration with the operating system
The JVM is the main reason Java is portable. Your source code is compiled into bytecode, and that bytecode can run on any machine with a compatible JVM.
### Runtime Architecture Diagram
```mermaid
flowchart LR
A[.java source files] --> B[javac compiler]
B --> C[.class bytecode]
C --> D[Class Loader]
D --> E[JVM Runtime]
E --> F[Interpreter]
E --> G[JIT Compiler]
G --> H[Native Machine Code]
E --> I[Garbage Collector]
E --> J[Heap and Thread Stacks]
```
### How It Works Internally
When you run a Java program, the JVM does not immediately convert every method into optimized native machine code. That would make startup too expensive. Instead, it usually starts by interpreting bytecode and watching which methods are "hot," meaning frequently executed. Hot code paths are then compiled by the Just-In-Time compiler into optimized native instructions.
That means Java has two important performance characteristics:
- startup can be slower than a small native binary because the runtime is initializing and warming up
- long-running applications can become very fast because the JVM gathers execution data and optimizes real usage patterns
This is why Java is a strong fit for backend services that run continuously for hours or days.
### Common Misconceptions
- "Java is purely interpreted." Not true. Java is compiled to bytecode, then interpreted and JIT-compiled at runtime.
- "Java is slow because it runs in a virtual machine." That is outdated thinking. For many long-running server workloads, modern JVM performance is excellent.
- "Installing Java means installing only the JVM." In practice, developers usually install a full JDK.
## A Minimal Java Program
```java
public class HelloApplication {
public static void main(String[] args) {
System.out.println("Hello, Java");
}
}
```
### What Each Part Means
- `public class HelloApplication`: defines a class named `HelloApplication`
- `public static void main(String[] args)`: the standard entry point for a standalone Java application
- `String[] args`: command-line arguments passed to the program
- `System.out.println(...)`: writes text to standard output
### Why Java Starts Here
Java treats execution as behavior that belongs inside a type. That is why even a simple program lives inside a class. In more advanced Java, you will also see records, enums, interfaces, and frameworks that manage object creation for you, but this class-based entry point is still the core model.
### Production Relevance
Real services often start with more than one line in `main`, but the high-level idea is similar:
- bootstrap configuration
- create or start the application container
- connect logging and monitoring
- register shutdown hooks
- start serving traffic
Spring Boot, for example, still starts with a `main` method, even though most of the heavy lifting is hidden behind the framework.
## Variables and Data Types
Types are one of Java's biggest strengths. A type tells both the compiler and the reader what kind of data a variable can hold and what operations are valid on that data.
### Intuition
You can think of types as contracts for data. They prevent a large class of bugs early. If a method expects an `int`, you cannot silently pass a string. If a variable holds a `Customer`, the IDE can show what methods and fields are available.
In fast-growing codebases, this matters a lot. The compiler becomes a guardrail against accidental misuse.
### Primitive Types
Primitive types store simple values directly.
| Type | Typical Use | Example |
| --- | --- | --- |
| `byte` | raw binary data, very small integers | `byte flags = 1;` |
| `short` | niche memory-sensitive numeric storage | `short yearOffset = 12;` |
| `int` | default integer arithmetic | `int itemCount = 42;` |
| `long` | large counters, timestamps, IDs | `long epochMillis = System.currentTimeMillis();` |
| `float` | specialized numeric work | `float ratio = 0.25f;` |
| `double` | default floating-point math | `double price = 19.99;` |
| `char` | individual UTF-16 code unit | `char grade = 'A';` |
| `boolean` | true/false logic | `boolean active = true;` |
### Reference Types
Reference types store a reference to an object, not the object value inline. Examples include:
- `String`
- arrays like `int[]`
- user-defined classes like `Order`
- interfaces like `List<String>`
- wrapper types like `Integer`
```java
int quantity = 5;
String status = "SHIPPED";
Order order = new Order();
```
Here:
- `quantity` directly holds a primitive value
- `status` holds a reference to a `String` object
- `order` holds a reference to an `Order` object
### Stack and Heap Mental Model
```mermaid
flowchart TD
A[Method invocation] --> B[Stack frame created]
B --> C[Local primitives stored directly]
B --> D[Local references stored]
D --> E[Objects allocated on heap]
E --> F[Shared until unreachable]
F --> G[Garbage collector reclaims memory]
```
This diagram is simplified, but it is good enough for a beginner mental model:
- local method state lives in a stack frame
- objects usually live on the heap
- references point to heap objects
- when objects are no longer reachable, garbage collection can reclaim them
### Why This Matters in Practice
If you accidentally keep references to objects longer than needed, memory usage grows. That is how some memory leaks happen in Java: not by forgetting `free()` like in C, but by retaining references in caches, static fields, thread locals, listeners, or long-lived collections.
### `var` and Type Inference
Modern Java supports local variable type inference with `var`.
```java
var customerName = "Priya";
var orderCount = 12;
```
This does not make Java dynamically typed. The compiler still infers a concrete static type.
Use `var` when the type is obvious from the right-hand side. Avoid it when it hides important meaning.
Bad:
```java
var result = service.execute(config, payload, strategy);
```
Better:
```java
PaymentResponse result = service.execute(config, payload, strategy);
```
### Common Pitfalls
- confusing primitives with wrapper types like `int` versus `Integer`
- assuming `null` is valid for primitives; it is not
- using floating-point types for money; production systems usually prefer `BigDecimal`
- overusing `var` until the code becomes harder to read
## Operators and Expressions
Operators are how you transform and compare values. Beginners often see them as simple symbols, but production bugs frequently come from operator misuse, especially around comparison, short-circuiting, and numeric behavior.
### Arithmetic Operators
Java supports familiar arithmetic operations:
```java
int total = 10 + 5;
int remaining = 10 - 3;
int doubled = 4 * 2;
int quotient = 9 / 2;
int remainder = 9 % 2;
```
Notice that `9 / 2` with integers becomes `4`, not `4.5`. This matters in billing, pagination, and rate calculations.
### Comparison Operators
```java
int threshold = 100;
boolean high = threshold > 80;
boolean exact = threshold == 100;
boolean different = threshold != 50;
```
For primitives, `==` compares values. For objects, `==` compares whether two references point to the same object.
That distinction is critical.
```java
String a = new String("ok");
String b = new String("ok");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
```
In domain code, use `equals()` for logical comparison unless you explicitly care about identity.
### Logical Operators
```java
boolean hasToken = true;
boolean notExpired = true;
boolean allowed = hasToken && notExpired;
boolean fallback = hasToken || notExpired;
boolean blocked = !hasToken;
```
`&&` and `||` short-circuit. That means Java may skip evaluating the right side if the left side already determines the result.
This is useful and common:
```java
if (user != null && user.isActive()) {
// safe because the second check only runs when user is not null
}
```
### Assignment and Increment Operators
```java
int attempts = 0;
attempts += 1;
attempts++;
```
Be careful with pre-increment and post-increment in larger expressions. They are legal, but often make code harder to reason about. Clear code is usually better than clever code.
### Ternary Operator
```java
String label = isAdmin ? "admin" : "user";
```
This is useful for simple conditional value selection. If the expression becomes nested or long, switch to an `if` block for readability.
### Pitfalls
- integer division truncates toward zero
- `==` on objects compares identity, not logical equality
- combining too many operators in one expression makes debugging harder
- side effects inside expressions reduce clarity
## Control Flow: If, Loops, and Switch
Control flow determines how your program makes decisions and repeats work. In backend systems, this often appears in validation logic, retry loops, data transformation, request routing, and state handling.
## Conditional Logic with `if`
```java
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
} else if (amount > 10_000) {
System.out.println("Manual review required");
} else {
System.out.println("Payment accepted");
}
```
### Why `if` Matters
In production code, many failures happen because conditions are incomplete, ordered incorrectly, or too hard to understand. Good conditionals are:
- explicit
- mutually understandable
- narrow in purpose
For example, authentication code often checks conditions in a deliberate order:
1. is the token present?
2. is the token well-formed?
3. is it expired?
4. does it map to a valid user?
That order affects both correctness and security.
## Loops
### `for` loop
```java
for (int index = 0; index < orders.size(); index++) {
System.out.println(orders.get(index));
}
```
Useful when you need the index or precise control.
### Enhanced `for` loop
```java
for (String email : emails) {
System.out.println(email);
}
```
Useful when you only need each element.
### `while` loop
```java
while (!queue.isEmpty()) {
process(queue.poll());
}
```
Useful when the stopping condition is not naturally tied to an index.
### Real-World Use Cases
- polling a message queue until empty in a batch job
- retrying an external API call with a maximum attempt count
- iterating through rows returned from a database or file
- walking through a list of events to build an aggregate state
### Pitfalls
- off-by-one errors in indexed loops
- modifying a collection incorrectly while iterating
- infinite loops caused by state that never changes
- putting expensive work inside nested loops without noticing the performance cost
If a loop processes millions of records, simple mistakes become production incidents.
## `switch`
Java's `switch` is useful when one variable determines multiple branches.
```java
String region = "EU";
switch (region) {
case "US":
System.out.println("Use US tax rules");
break;
case "EU":
System.out.println("Use EU VAT rules");
break;
default:
System.out.println("Use global defaults");
}
```
Modern Java also supports switch expressions, which are often cleaner.
```java
String action = switch (region) {
case "US" -> "usd-pricing";
case "EU" -> "eur-pricing";
default -> "default-pricing";
};
```
### Why This Matters in Production
`switch` logic often appears in:
- request routing based on event type
- status-to-action mapping
- feature behavior by region or tenant type
- serialization and parsing logic
The main design risk is letting a `switch` grow so large that it becomes a code smell. At some point, polymorphism or strategy objects become a better fit.
## Methods and Basic Program Structure
Methods are where Java code becomes reusable, testable, and readable.
### Intuition
A method should represent one coherent action. When methods are too large, names stop helping and reasoning becomes difficult. Good Java code often feels readable because each method does one thing at the right level of abstraction.
### Example
```java
public class InvoiceService {
public double calculateTotal(double subtotal, double taxRate) {
validateSubtotal(subtotal);
return subtotal + (subtotal * taxRate);
}
private void validateSubtotal(double subtotal) {
if (subtotal < 0) {
throw new IllegalArgumentException("Subtotal cannot be negative");
}
}
}
```
### Why This Structure Is Better
Instead of mixing validation, math, logging, and persistence in one method, this design separates concerns. That gives you:
- clearer intent
- easier unit testing
- simpler debugging
- less duplication when validation rules are reused
### Method Signature Elements
- access modifier: `public`, `private`, and so on
- return type: `double`, `String`, custom types, or `void`
- method name: should describe behavior
- parameters: inputs required by the method
- thrown exceptions: sometimes declared explicitly
### Parameter Passing in Java
Java is always pass-by-value.
For primitives, the value itself is copied.
For objects, the reference is copied. That copied reference still points to the same underlying object.
```java
class Counter {
int value;
}
void increment(Counter counter) {
counter.value++;
}
```
If you call `increment(counter)`, the original object changes because both the caller and callee reference the same object. But if the method assigns `counter = new Counter();`, the caller does not start pointing to that new object.
This is a very common interview topic and a very common source of beginner confusion.
### Common Method Design Mistakes
- methods that do too many things
- boolean flags like `process(true, false, true)` that make call sites unreadable
- hidden side effects such as mutating shared state unexpectedly
- returning `null` casually when an empty collection, exception, or `Optional` would be clearer
## Packages, Imports, and Source Organization
As Java codebases grow, organization matters as much as syntax.
### Packages
Packages group related classes.
```java
package com.example.orders;
```
In real systems, packages usually reflect bounded areas of responsibility, such as:
- `com.company.auth`
- `com.company.billing`
- `com.company.notifications`
### Imports
Imports let you refer to classes without writing their fully qualified names everywhere.
```java
import java.time.Instant;
import java.util.List;
```
### Real-World Convention
In production services, code is usually organized around features or layers. A simple service might contain packages for:
- controllers or API endpoints
- services or business logic
- repositories or persistence
- domain models
- configuration
That structure becomes much easier to reason about once you already understand classes, methods, and object interactions.
## Build Tools and the Everyday Java Workflow
You can compile Java with `javac` directly, but professional projects almost always use a build tool.
### Maven and Gradle
These tools handle:
- dependency downloads
- compilation
- test execution
- packaging into JARs
- plugin-based workflows like code generation or static analysis
### Why This Matters
In a real backend service, you rarely work with one file in isolation. Your build tool defines how the application is assembled, which library versions are used, and what happens in CI before code is deployed.
If Java syntax is the grammar, Maven or Gradle is part of the operating system of day-to-day Java development.
## How Fundamentals Show Up in Production Systems
The concepts in this file are not classroom-only topics.
- Strong typing makes API contracts and refactors safer.
- JVM portability allows the same service artifact to run in local development, test, and cloud environments.
- Control flow drives validation, retries, workflows, and business rules.
- Method design strongly affects maintainability and testability.
- Correct type choice matters for money, timestamps, concurrency, and memory usage.
For example, an order-processing service may:
1. start inside a JVM launched by a container image
2. receive a request into a controller method
3. validate fields with `if` conditions
4. loop through order items
5. call helper methods to calculate totals and taxes
6. store domain objects on the heap while processing the request
That is not advanced Java. That is Java fundamentals in production.
## Key Takeaways
- Java's real strength is not brevity. It is explicit structure, strong tooling, and predictable runtime behavior.
- The JDK is your development toolkit, the JVM is the runtime engine, and bytecode portability is central to how Java works.
- Primitive types and reference types behave differently, and understanding heap versus stack is essential for reasoning about memory.
- Operators and control flow look simple, but many real bugs come from incorrect comparisons, truncation, bad branching, or poorly designed loops.
- Methods are the unit of reusable behavior, and clean method design has direct impact on readability, testing, and long-term maintenance.
- These fundamentals appear everywhere in real backend systems, so learning them deeply pays off before moving on to OOP, collections, concurrency, and frameworks.
@@ -0,0 +1,554 @@
# File 2: Object-Oriented Programming in Java
Object-oriented programming is where Java starts to feel like Java. The syntax from the fundamentals file matters, but OOP is what gives Java its shape in real applications. Backend systems are rarely a loose collection of standalone functions. They are usually modeled as collaborating objects: controllers, services, repositories, domain entities, validators, clients, schedulers, and configuration components.
If you only memorize the four textbook pillars of OOP, you will miss the point. The real question is this: how do we organize code so behavior stays understandable as the system grows? This file answers that question from a Java engineer's perspective.
## Classes and Objects
### Intuition
A class is a blueprint for state and behavior. An object is a concrete instance created from that blueprint.
That definition is technically correct, but the useful mental model is stronger:
- a class defines a unit of responsibility
- an object is one live participant carrying out that responsibility at runtime
For example, in an order-processing service:
- `Order` might represent business data
- `OrderService` might represent business behavior
- `EmailNotifier` might represent an integration with an external system
### Example
```java
public class BankAccount {
private final String accountNumber;
private double balance;
public BankAccount(String accountNumber, double openingBalance) {
this.accountNumber = accountNumber;
this.balance = openingBalance;
}
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance += amount;
}
public void withdraw(double amount) {
if (amount <= balance) {
balance -= amount;
}
}
public double getBalance() {
return balance;
}
}
```
### Why This Matters
Notice that the account is not just a bag of fields. It contains rules. That is one of the key ideas in OOP: data and behavior should often live together so the invariants are easier to protect.
In real systems, that helps prevent invalid state changes from being scattered across the codebase.
## Encapsulation
Encapsulation means hiding internal state and exposing only the operations that make sense.
### Why It Exists
If every part of the system can freely mutate every field, the object stops being trustworthy. You can no longer tell where a bug came from because the object's state could have been changed anywhere.
Encapsulation creates a boundary:
- internal representation is private
- changes happen through controlled methods
- validation and invariants live close to the data they protect
### Example
Bad design:
```java
public class UserProfile {
public String email;
public boolean verified;
}
```
Any caller can put this object into inconsistent states.
Better design:
```java
public class UserProfile {
private String email;
private boolean verified;
public UserProfile(String email) {
changeEmail(email);
this.verified = false;
}
public void changeEmail(String newEmail) {
if (newEmail == null || newEmail.isBlank()) {
throw new IllegalArgumentException("Email is required");
}
this.email = newEmail;
this.verified = false;
}
public void markVerified() {
this.verified = true;
}
public String getEmail() {
return email;
}
public boolean isVerified() {
return verified;
}
}
```
### Real-World Use Case
Encapsulation is critical in domains such as:
- payments, where balance or ledger state must obey strict rules
- authentication, where token and session transitions must remain valid
- inventory, where stock cannot drop below zero because random code mutated a field directly
### Common Misconception
Encapsulation is not the same as "always write getters and setters for every field." Blindly exposing setters often destroys the protection that encapsulation is supposed to provide. A class should expose behavior, not just field mutation.
## Inheritance
Inheritance lets one class reuse and extend behavior from another class.
```java
public class Employee {
protected final String name;
public Employee(String name) {
this.name = name;
}
public void work() {
System.out.println(name + " is working");
}
}
public class Manager extends Employee {
public Manager(String name) {
super(name);
}
@Override
public void work() {
System.out.println(name + " is planning and reviewing work");
}
}
```
### Intuition
Inheritance represents an "is-a" relationship.
- a `Manager` is an `Employee`
- a `Circle` is a `Shape`
The subclass gets access to behavior from the parent and may override methods.
### How It Works Internally
When you call an overridden method on a parent reference, Java uses dynamic dispatch at runtime to choose the actual implementation based on the real object type, not just the variable type.
```java
Employee employee = new Manager("Asha");
employee.work();
```
This calls `Manager.work()`, not `Employee.work()`.
### Where Inheritance Helps
Inheritance is useful when subclasses truly share a stable conceptual model and substantial reusable behavior. Typical examples:
- framework base classes
- exception hierarchies
- UI component hierarchies in some systems
- domain modeling where a clear subtype relationship exists
### Why Engineers Are Careful with It
Inheritance creates tight coupling. Once a subclass depends on parent implementation details, changes become harder. Deep inheritance trees are often brittle and confusing.
That is why experienced Java engineers often prefer composition unless the inheritance relationship is very natural.
## Polymorphism
Polymorphism means code can work against a general type while runtime behavior changes based on the actual object implementation.
### Example
```java
public interface PaymentProcessor {
PaymentResult process(PaymentRequest request);
}
public class CardPaymentProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult("card-authorized");
}
}
public class WalletPaymentProcessor implements PaymentProcessor {
@Override
public PaymentResult process(PaymentRequest request) {
return new PaymentResult("wallet-authorized");
}
}
```
Now calling code can depend on `PaymentProcessor` without caring which concrete implementation is active.
```java
public class CheckoutService {
private final PaymentProcessor paymentProcessor;
public CheckoutService(PaymentProcessor paymentProcessor) {
this.paymentProcessor = paymentProcessor;
}
public PaymentResult checkout(PaymentRequest request) {
return paymentProcessor.process(request);
}
}
```
### Production Relevance
This pattern shows up everywhere:
- different payment providers behind one contract
- multiple notification channels behind one interface
- storage implementations for local disk, S3, or blob storage
- authentication strategies for API key, OAuth, or internal service token
Polymorphism reduces branching and keeps behavior extensible.
## Abstraction
Abstraction means exposing the essential behavior while hiding unnecessary implementation detail.
### Intuition
When you drive a car, you use the steering wheel and pedals, not the combustion process details. In code, abstraction gives callers the same kind of high-level interface.
### Abstract Class Example
```java
public abstract class ReportGenerator {
public final String generate() {
fetchData();
transformData();
return renderOutput();
}
protected abstract void fetchData();
protected abstract void transformData();
protected abstract String renderOutput();
}
```
This is useful when you want a common workflow with customizable steps.
### Real-World Use Case
Frameworks use abstraction heavily. A framework might define the overall lifecycle and let your code implement only the project-specific pieces.
That idea appears in:
- servlet filters
- batch job processing
- event handlers
- test frameworks
- template methods inside enterprise applications
## Interfaces vs Abstract Classes
This is a classic Java question, but it is more valuable when framed as a design decision rather than a quiz answer.
### Interface
An interface defines a contract. It says what behavior a type must support.
```java
public interface Notifier {
void send(String destination, String message);
}
```
### Abstract Class
An abstract class is a partial implementation. It is useful when related types share both behavior and state.
```java
public abstract class BaseNotifier {
protected final AuditLogger auditLogger;
protected BaseNotifier(AuditLogger auditLogger) {
this.auditLogger = auditLogger;
}
protected void recordAudit(String destination) {
auditLogger.record("sent to " + destination);
}
}
```
### Practical Comparison
| Topic | Interface | Abstract Class |
| --- | --- | --- |
| Main purpose | Define capability or role | Share base behavior and state |
| Multiple inheritance | A class can implement many | A class can extend only one |
| Instance fields | No regular instance state | Yes |
| Constructor | No | Yes |
| Best use | Flexible contracts | Common base workflow or shared implementation |
### Design Intuition
Use an interface when you want callers to depend on behavior and stay decoupled from implementation details.
Use an abstract class when multiple related types genuinely share implementation and that shared base is stable enough to matter.
In many backend codebases, interfaces are common for boundaries and abstract classes are used more selectively.
## Composition vs Inheritance
This is one of the most important design instincts in Java.
### Composition
Composition means building objects by combining other objects.
```java
public class OrderService {
private final PaymentProcessor paymentProcessor;
private final InventoryService inventoryService;
private final NotificationService notificationService;
public OrderService(
PaymentProcessor paymentProcessor,
InventoryService inventoryService,
NotificationService notificationService) {
this.paymentProcessor = paymentProcessor;
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
}
```
The service is not inheriting behavior. It is coordinating collaborators.
### Why Composition Usually Wins
Composition is often more flexible because:
- dependencies are explicit
- behavior can be swapped at runtime or in tests
- classes stay focused on one role
- you avoid fragile parent-child coupling
### Relationship Diagram
```mermaid
classDiagram
class OrderService {
-PaymentProcessor paymentProcessor
-InventoryService inventoryService
-NotificationService notificationService
+placeOrder()
}
class PaymentProcessor {
<<interface>>
+process(request)
}
class CardPaymentProcessor {
+process(request)
}
class InventoryService {
+reserve(items)
}
class NotificationService {
+sendConfirmation(order)
}
OrderService --> PaymentProcessor : uses
OrderService --> InventoryService : uses
OrderService --> NotificationService : uses
CardPaymentProcessor ..|> PaymentProcessor
```
### Inheritance Diagram
```mermaid
classDiagram
class Employee {
+work()
}
class Manager {
+work()
+approveBudget()
}
class Engineer {
+work()
+buildFeature()
}
Employee <|-- Manager
Employee <|-- Engineer
```
### Rule of Thumb
If the relationship is naturally "has-a," use composition.
If the relationship is naturally "is-a" and the base behavior is stable and meaningful, inheritance may be fine.
### Common Pitfall
Do not use inheritance just to avoid writing a few lines twice. Duplication can often be refactored in other ways. Wrong inheritance is more expensive than small duplication.
## Object Construction and Lifecycle
In Java, objects are created with `new`, by factories, or by frameworks that manage dependency injection.
### Basic Construction
```java
UserProfile profile = new UserProfile("user@example.com");
```
### Why Construction Matters in Real Systems
As applications grow, object creation itself becomes design-relevant.
- should every caller build the object manually?
- should construction enforce invariants?
- should expensive resources be shared?
- should a framework control creation for testability and lifecycle management?
This is why design patterns such as builder, factory, and dependency injection appear so often in Java engineering.
## `this`, `super`, and Method Overriding
### `this`
`this` refers to the current object.
```java
public class Customer {
private final String name;
public Customer(String name) {
this.name = name;
}
}
```
### `super`
`super` refers to the parent class portion of the object.
```java
public class PremiumCustomer extends Customer {
public PremiumCustomer(String name) {
super(name);
}
}
```
### Overriding Rules Worth Remembering
- method name and parameters must match
- return type must be compatible
- access level cannot be more restrictive
- static methods are hidden, not overridden
- constructors are not overridden
These rules matter because subtle mistakes can lead to code that compiles but does not behave the way you expected.
## OOP Design Intuition
Strong OOP design is less about showing every language feature and more about assigning responsibility well.
### Good Design Questions
- which object should own this rule?
- which class is holding too much knowledge?
- is this behavior better represented as a method, a collaborator, or a separate strategy?
- are we modeling domain concepts clearly, or just dumping data into DTO-like objects?
### Signs of Weak OOP Design
- giant service classes that do everything
- data classes with no behavior and business logic scattered elsewhere
- excessive inheritance used for convenience rather than meaning
- public mutable fields everywhere
- interfaces added for every class even when no abstraction need exists
### Production Perspective
In real backend systems, the best OOP often looks boring in a good way:
- classes have narrow responsibilities
- collaborators are injected explicitly
- domain rules live near the domain model
- interfaces define important boundaries
- composition is preferred over deep inheritance
That style scales better than clever class hierarchies.
## How OOP Shows Up in a Backend Request
When an HTTP request arrives in a Java service, several objects usually collaborate:
```mermaid
flowchart LR
A[HTTP Request] --> B[Controller]
B --> C[Service]
C --> D[Repository]
C --> E[PaymentProcessor]
C --> F[NotificationService]
D --> G[(Database)]
E --> H[External Provider]
```
Each object has a distinct role. That separation is one of the main benefits of object-oriented design in Java. The controller handles transport concerns, the service handles business workflow, the repository handles persistence, and integrations are wrapped behind focused components.
## Key Takeaways
- A class should represent a coherent responsibility, and an object is a runtime participant carrying out that responsibility.
- Encapsulation protects invariants and is more valuable than blindly generating getters and setters.
- Inheritance enables reuse and polymorphism, but it also creates tight coupling and should be used carefully.
- Interfaces are excellent for contracts and system boundaries, while abstract classes are better for shared base behavior and state.
- Composition is usually more flexible and maintainable than inheritance in production Java systems.
- Good OOP design is about clear responsibility and collaboration, not about maximizing the number of classes or language features used.
+447
View File
@@ -0,0 +1,447 @@
# File 3: Core Java APIs
The Java language by itself is only part of the story. What makes Java productive in real work is the standard library. Once you move beyond syntax, you spend large amounts of time with `String`, collections, generics, exceptions, file and stream APIs, time APIs, and utility classes.
This file focuses on the APIs that show up constantly in real applications. These topics are foundational because they influence how data moves through the system, how failures are handled, and how code remains readable and type-safe at scale.
## Strings and Immutability
### Intuition
A `String` is one of the most frequently used objects in Java. It represents text such as user names, JSON fields, URLs, tokens, file paths, SQL fragments, log messages, and HTTP headers.
Java makes `String` immutable, which means once a string object is created, its contents do not change.
### Why Immutability Matters
Immutability makes strings:
- safer to share across threads
- easier to reason about
- more predictable as map keys and cache keys
- less error-prone in APIs that pass text through multiple layers
### Example
```java
String original = "order";
String upper = original.toUpperCase();
System.out.println(original); // order
System.out.println(upper); // ORDER
```
`original` is unchanged. `toUpperCase()` returns a new `String`.
### Internal Detail That Matters
Java often interns string literals, meaning identical literals may refer to the same pooled object. That is an optimization detail, not something business logic should rely on.
This is why using `==` for strings is dangerous.
```java
String a = new String("paid");
String b = new String("paid");
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
```
### Performance Concern
Repeated string concatenation inside loops can create many temporary objects.
```java
String result = "";
for (String item : items) {
result += item;
}
```
Prefer `StringBuilder` for heavy concatenation.
```java
StringBuilder builder = new StringBuilder();
for (String item : items) {
builder.append(item);
}
String result = builder.toString();
```
### Real-World Use Cases
- building log lines or audit entries
- parsing and validating incoming request fields
- constructing file paths, cache keys, and search queries
- formatting API responses or templated messages
### Common Pitfalls
- comparing strings with `==`
- forgetting that strings are immutable and assuming a method call mutates them
- building large strings inefficiently in loops
## Collections Framework
Collections are how Java code represents groups of related elements. In real systems, almost every workflow involves a collection somewhere: request headers, order items, search results, cache entries, event batches, deduplicated IDs, grouped metrics, or lookup tables.
### Collections Structure Diagram
```mermaid
flowchart TD
A[Collection Framework] --> B[List]
A --> C[Set]
A --> D[Map]
B --> E[ArrayList]
B --> F[LinkedList]
C --> G[HashSet]
C --> H[TreeSet]
D --> I[HashMap]
D --> J[TreeMap]
```
### `List`
A `List` is ordered and allows duplicates.
```java
List<String> steps = new ArrayList<>();
steps.add("validate");
steps.add("charge");
steps.add("notify");
```
Use a list when order matters or when repeated values are acceptable.
#### `ArrayList`
This is the default `List` choice in most business applications. It provides fast indexed access and good general-purpose performance.
Typical use cases:
- ordered API results
- batched work items
- DTO lists in controller responses
#### `LinkedList`
Used far less often in typical backend applications than beginners expect. It can be helpful in queue-like patterns, but `ArrayList` and dedicated queue implementations are usually more practical.
### `Set`
A `Set` stores unique elements.
```java
Set<String> roles = new HashSet<>();
roles.add("ADMIN");
roles.add("USER");
roles.add("ADMIN");
```
The duplicate `ADMIN` is ignored.
Use a set when uniqueness matters.
Real-world examples:
- deduplicating email addresses
- tracking processed event IDs for idempotency checks
- storing permission names
### `Map`
A `Map` stores key-value pairs.
```java
Map<String, Integer> inventory = new HashMap<>();
inventory.put("laptop", 15);
inventory.put("mouse", 48);
```
Maps are everywhere in backend systems.
Typical examples:
- ID to entity lookup
- configuration name to value
- region to tax rule
- user ID to session metadata
### Choosing Common Implementations
| Type | Default Choice | Why |
| --- | --- | --- |
| `List` | `ArrayList` | Fast iteration and index-based reads |
| `Set` | `HashSet` | Fast uniqueness checks |
| `Map` | `HashMap` | Fast key-based lookup |
Choose tree-based variants like `TreeSet` or `TreeMap` when you explicitly need sorted ordering.
### Practical Usage Pattern
Suppose you receive a batch of orders and want to group them by customer.
```java
Map<String, List<Order>> ordersByCustomer = new HashMap<>();
for (Order order : orders) {
ordersByCustomer
.computeIfAbsent(order.getCustomerId(), key -> new ArrayList<>())
.add(order);
}
```
This is a very common pattern in reporting, aggregation, and batch-processing pipelines.
### Pitfalls
- assuming `HashMap` preserves insertion order; it does not
- forgetting that mutable keys can break map behavior
- choosing a list when you really need uniqueness or fast lookup
- modifying a collection while iterating in unsupported ways, causing `ConcurrentModificationException`
## Generics
Generics allow Java to express type-safe reusable data structures and APIs.
### Intuition
Without generics, collections would hold generic `Object` values, forcing you to cast manually and discover mistakes late. Generics move those checks to compile time.
```java
List<String> names = new ArrayList<>();
names.add("Anita");
```
Now the compiler knows this list is supposed to contain strings.
### Why This Matters in Real Systems
Generics make API contracts clearer.
Examples:
- `List<Order>` means a list of orders, not arbitrary objects
- `Map<String, UserSession>` means string keys mapped to session objects
- `ResponseEntity<CustomerDto>` clearly communicates response payload type in a web application
### Generic Methods
```java
public static <T> T first(List<T> items) {
if (items.isEmpty()) {
throw new IllegalArgumentException("List cannot be empty");
}
return items.get(0);
}
```
### Wildcards at a High Level
You will eventually see forms such as `List<? extends Number>` or `List<? super Integer>`. The full details take practice, but the intuition is:
- `extends` is for reading from a producer
- `super` is for writing to a consumer
This is often summarized as PECS: producer extends, consumer super.
### Internal Note
Java generics use type erasure, meaning generic type information is mostly removed at runtime. This is why you cannot do everything with generics that reified generic systems in some other languages allow.
### Common Pitfalls
- using raw types like `List` instead of `List<String>`
- overcomplicating code with advanced wildcards when a simpler API would do
- expecting generic type arguments to remain fully available at runtime
## Exception Handling
Failure handling is a core part of Java engineering. Good code does not just describe the happy path. It makes failure modes visible and deliberate.
### The Idea Behind Exceptions
An exception represents a failure or abnormal condition that interrupts normal execution.
Some failures are expected operationally, such as missing files or invalid user input. Others represent programming bugs, such as null dereferences or invalid assumptions.
### Checked vs Unchecked Exceptions
Java has two broad exception categories.
#### Checked exceptions
These are enforced by the compiler. You must catch them or declare them.
Examples include many file and I/O related exceptions.
#### Unchecked exceptions
These extend `RuntimeException`. The compiler does not force handling.
Examples include:
- `NullPointerException`
- `IllegalArgumentException`
- `IllegalStateException`
### Why the Distinction Exists
Checked exceptions signal recoverable or expected external failure modes.
Unchecked exceptions often represent programming mistakes or invalid internal states.
In real engineering, teams vary on how much they like checked exceptions, but understanding the model is still important.
### Example
```java
public String readFile(Path path) throws IOException {
return Files.readString(path);
}
```
The caller must decide whether to handle `IOException` or propagate it.
### Practical Guidance
- throw exceptions that communicate intent clearly
- do not catch exceptions just to ignore them
- wrap low-level exceptions when exposing a cleaner domain-level abstraction
- include useful context in logs and messages
### Real-World Pattern
Suppose a service calls a third-party payment gateway. The low-level HTTP client might throw transport exceptions, but your business layer may convert those into a domain-specific exception like `PaymentUnavailableException`.
That makes the rest of the system easier to understand.
### `try`, `catch`, `finally`, and try-with-resources
```java
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.readLine();
} catch (IOException exception) {
throw new IllegalStateException("Failed to read config", exception);
}
```
The try-with-resources form is important because it closes resources automatically.
### Pitfalls
- catching `Exception` too broadly and hiding real issues
- using exceptions for normal control flow
- logging and rethrowing the same exception repeatedly, creating noisy duplicate logs
- losing the original cause when wrapping exceptions badly
## Java I/O Basics
I/O means interacting with systems outside your program's in-memory state.
Examples include:
- reading files
- writing logs
- handling network sockets
- streaming data to cloud storage
- loading configuration from disk
### Intuition
I/O is slower and less predictable than pure in-memory operations because it depends on disks, networks, operating system buffers, remote services, and external state.
That is why I/O-heavy code needs stronger error handling and performance awareness.
### Core Modern APIs
The `java.nio.file` package is the common modern entry point for file work.
```java
Path path = Path.of("config/app.properties");
String content = Files.readString(path);
Files.writeString(path, content + "\nmode=prod");
```
### Streams of Data
Buffered APIs are often used for efficient reading and writing.
```java
try (BufferedReader reader = Files.newBufferedReader(path)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
```
### Production Use Cases
- loading service configuration or templates at startup
- ingesting CSV or log files in batch jobs
- exporting reports to disk or object storage
- reading secrets or certificates mounted into a container
### Common Pitfalls
- reading huge files into memory when streaming would be safer
- forgetting character encoding concerns
- failing to close resources properly
- performing blocking I/O on critical request threads without understanding throughput impact
## Practical Usage Patterns That Show Up Everywhere
### Pattern 1: Deduplication
Use a `Set` when idempotency or uniqueness matters.
```java
Set<String> processedEventIds = new HashSet<>();
```
### Pattern 2: Lookup by Key
Use a `Map` for fast retrieval.
```java
Map<Long, Customer> customersById = new HashMap<>();
```
### Pattern 3: Ordered Results
Use a `List` when sequence matters.
```java
List<Transaction> transactions = new ArrayList<>();
```
### Pattern 4: Safe Text Handling
Use immutable strings for identifiers, payload fragments, and logs, but switch to `StringBuilder` for heavy concatenation.
### Pattern 5: Resource Safety
Use try-with-resources for anything that must be closed, such as readers, streams, sockets, and many database-facing abstractions.
## How These APIs Show Up in Production Systems
Consider a batch job that processes order exports:
1. it reads a file using Java I/O APIs
2. it parses each line into objects and stores them in collections
3. it uses maps for lookups and sets for deduplication
4. it uses strings to validate and normalize text fields
5. it throws or wraps exceptions when bad data or file issues occur
6. it emits a summary report built safely and efficiently
This is ordinary Java engineering. It is not glamorous, but a large amount of production reliability depends on doing these basics well.
## Key Takeaways
- `String` is immutable, which improves safety and predictability, but you should still be mindful of comparison and concatenation costs.
- The collections framework gives you distinct tools for ordering, uniqueness, and lookup, and choosing the right one affects both clarity and performance.
- Generics move many type errors to compile time and make library and application code much easier to reason about.
- Exception handling is about making failure explicit and meaningful, not just preventing crashes.
- Java I/O interacts with slow and failure-prone external systems, so resource handling and error handling matter a lot.
- These APIs form the everyday vocabulary of production Java code, especially in backend services, batch jobs, and integration-heavy systems.
+384
View File
@@ -0,0 +1,384 @@
# File 4: Advanced Java Concepts
This file covers the topics that make Java powerful in long-running, high-throughput systems: concurrency, the memory model, garbage collection, streams, and performance thinking. These are the areas where beginner knowledge often stops and engineering maturity starts.
You do not need to become a JVM internals expert on day one, but you do need a solid mental model. Otherwise, concurrency bugs, latency spikes, and memory issues will feel mysterious. The goal here is to remove that mystery.
## Multithreading and Concurrency
### Intuition
A thread is an independent path of execution inside a process. Java supports multiple threads so a program can do more than one thing at the same time or keep making progress while other work waits on I/O.
In a backend service, threads may handle:
- incoming HTTP requests
- scheduled jobs
- asynchronous message processing
- background cleanup work
- database or network calls coordinated by worker pools
### Why Concurrency Exists
Without concurrency, a service that waits on slow operations would waste time doing nothing. With concurrency, other work can proceed while one task waits.
### Basic Example
```java
Runnable task = () -> System.out.println("Processing in " + Thread.currentThread().getName());
Thread worker = new Thread(task);
worker.start();
```
This creates and starts a new thread, but in production systems you usually prefer executors and thread pools over manually creating raw threads.
### Thread Pool Mental Model
Instead of constantly creating and destroying threads, a thread pool reuses a fixed or managed set of worker threads.
That reduces overhead and gives you control over concurrency level.
```java
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> processOrder(orderId));
```
### Real-World Use Cases
- processing multiple independent tasks in parallel
- handling high request volume in servers
- offloading email or report generation from the request thread
- consuming events from a queue with worker threads
### Common Pitfall
More threads do not automatically mean better performance. Too many threads can increase context switching, memory pressure, lock contention, and tail latency.
## Thread Lifecycle
```mermaid
stateDiagram-v2
[*] --> New
New --> Runnable: start()
Runnable --> Running: scheduled by CPU
Running --> Blocked: waiting for lock or I/O
Blocked --> Runnable: lock or I/O available
Running --> Waiting: wait/sleep/join
Waiting --> Runnable: notified or timeout
Running --> Terminated: run completes
```
### Why the Lifecycle Matters
If you debug a production incident involving stuck requests or slow jobs, understanding whether threads are runnable, blocked, waiting, or deadlocked becomes very important. Tools like thread dumps only make sense if you understand these states.
## Synchronization
Concurrency becomes difficult when multiple threads access shared mutable state.
### The Core Problem
Imagine two threads incrementing the same counter.
```java
counter++;
```
This looks atomic, but it is actually multiple steps:
1. read current value
2. add one
3. write new value back
If two threads interleave these steps, updates can be lost.
### `synchronized`
Java's `synchronized` keyword provides mutual exclusion and visibility guarantees.
```java
public class Counter {
private int value;
public synchronized void increment() {
value++;
}
public synchronized int getValue() {
return value;
}
}
```
Only one thread can execute a synchronized method or block on the same monitor at a time.
### Why It Works
Synchronization does two important things:
- prevents unsafe concurrent access to the protected region
- ensures memory visibility so threads see up-to-date values when entering and leaving synchronized regions
### Real-World Use Cases
- protecting shared in-memory caches or mutable counters
- coordinating state transitions in schedulers and worker systems
- ensuring thread-safe updates in domain objects used by multiple threads
### Common Pitfall
Synchronizing too broadly can kill throughput. Synchronizing too narrowly can fail to protect the actual shared invariant.
## Locks and Higher-Level Concurrency Utilities
Beyond `synchronized`, Java provides explicit lock types and concurrency utilities in `java.util.concurrent`.
### `ReentrantLock`
This can be useful when you need features beyond simple intrinsic locking, such as timed lock acquisition or finer control.
```java
Lock lock = new ReentrantLock();
lock.lock();
try {
updateSharedState();
} finally {
lock.unlock();
}
```
### Other Important Utilities
- `AtomicInteger`, `AtomicLong`: lock-free atomic updates for simple counters and state
- `ConcurrentHashMap`: concurrent map implementation for many common shared lookup patterns
- `CountDownLatch`: wait until several tasks complete
- `Semaphore`: limit concurrent access to a resource
- `CompletableFuture`: compose asynchronous operations more cleanly
### Production Relevance
These utilities are common in rate limiting, request fan-out, concurrent caches, asynchronous orchestration, and worker coordination.
## `volatile`
`volatile` is often misunderstood.
### What It Guarantees
A volatile field provides visibility. When one thread writes a new value, other threads reading that field will see the latest value.
```java
private volatile boolean running = true;
```
This is useful for simple state flags.
### What It Does Not Guarantee
`volatile` does not make compound operations atomic.
This is still unsafe:
```java
volatile int count;
count++;
```
The increment is still a read-modify-write sequence.
### Good Use Case
Stopping a background loop cleanly:
```java
while (running) {
pollQueue();
}
```
### Bad Use Case
Using `volatile` as a replacement for proper locking around shared mutable objects.
## JVM Memory Model
The Java Memory Model explains how threads interact through memory and what guarantees exist around reads, writes, ordering, and visibility.
### Intuition
In a multithreaded system, one thread can update a value, but another thread may not immediately observe that update unless the program uses the right synchronization rules.
This is because CPUs, caches, compilers, and runtimes all optimize memory access.
### Diagram
```mermaid
flowchart LR
A[Main Memory] --> B[Thread 1 Working Memory]
A --> C[Thread 2 Working Memory]
B --> D[Local reads and writes]
C --> E[Local reads and writes]
F[synchronized or volatile] --> A
```
### Why Engineers Care
Without the memory model, some code would appear to "work on my machine" and fail under load or on different hardware.
The important practical lesson is not to memorize the whole specification. The important lesson is:
- do not share mutable state casually between threads
- use proper synchronization tools when you do share it
- visibility and atomicity are different concerns
## Garbage Collection
Java manages memory automatically through garbage collection, but automatic does not mean irrelevant.
### Intuition
Instead of manually freeing objects, Java tracks which objects are still reachable. Unreachable objects become candidates for collection.
### Simplified Heap View
```mermaid
flowchart TD
A[Application creates objects] --> B[Objects live on heap]
B --> C[Reachable from roots]
B --> D[Unreachable objects]
D --> E[Garbage collector reclaims memory]
```
### What Are Roots
Objects are considered reachable if they can be reached from GC roots such as:
- active thread stacks
- static references
- JNI references and runtime internals
### Why This Matters in Real Systems
GC behavior affects:
- latency spikes
- memory footprint
- throughput
- container sizing decisions
### Common Memory Problems in Java
- retaining objects in caches without eviction
- storing large collections in long-lived singletons
- leaking listeners or thread-local data
- creating too many short-lived temporary objects in hot code paths
### Misconception
"Java cannot have memory leaks because it has garbage collection" is false. Java absolutely can have memory leaks if your code keeps references to objects that are no longer useful.
## Streams API and Functional Programming Style
Java's Streams API provides a declarative way to process collections and sequences of data.
### Intuition
Instead of describing every loop step manually, you describe a pipeline of transformations.
```java
List<String> emails = users.stream()
.filter(User::isActive)
.map(User::getEmail)
.sorted()
.toList();
```
### What Happens Conceptually
The stream pipeline is lazy until a terminal operation runs. Operations like `filter` and `map` describe work. A terminal operation such as `toList()`, `count()`, or `forEach()` triggers execution.
### Why This Is Useful
Streams can make transformation pipelines clearer when the logic is naturally data-oriented.
Real-world examples:
- filtering valid requests
- mapping entities to DTOs
- aggregating metrics
- grouping records for reporting
### When Streams Help
- straightforward transformations and aggregations
- collection processing where each step is conceptually distinct
- code that benefits from a pipeline style
### When Plain Loops Are Better
- highly stateful logic
- error handling that becomes awkward in a stream chain
- performance-sensitive paths where allocation and readability must be controlled carefully
### Common Pitfalls
- overusing streams for complex business workflows that become unreadable
- assuming streams are always faster than loops
- using shared mutable state inside stream operations
- misunderstanding laziness and side effects
## Performance Considerations
Performance in Java is not just about writing "fast code." It is about understanding tradeoffs across CPU, memory, I/O, latency, and concurrency.
### Key Areas to Watch
- object allocation rate
- unnecessary boxing and unboxing
- poor data structure choice
- lock contention
- blocking I/O on critical threads
- large heap pressure and GC pauses
### Example: Data Structure Choice Matters
If you repeatedly test membership in a list of one million IDs, a `HashSet` is usually a much better fit than a `List` because lookup behavior is different.
### Example: Avoiding Work on Hot Paths
If a request path is called tens of thousands of times per second, avoid unnecessary logging, object churn, repeated parsing, or expensive string formatting.
### Production Perspective
Java performance work should be evidence-driven. Good engineers measure before changing code. They use:
- metrics and tracing
- profiling tools
- thread dumps
- heap dumps
- realistic load tests
Premature optimization is a problem, but ignoring obvious bottlenecks is also a problem. Performance work is about judgment, not superstition.
## How Advanced Java Shows Up in Real Systems
Consider a service that consumes messages from a queue:
1. a thread pool pulls messages concurrently
2. shared state such as rate limits or caches needs safe access
3. visibility and ordering matter between worker threads
4. processed payloads create object allocations on the heap
5. stream pipelines may transform batches for enrichment or filtering
6. GC and lock contention affect latency under load
This is why advanced Java matters. These are not academic concerns. They directly influence system correctness and performance.
## Key Takeaways
- Concurrency is about safely making progress with multiple threads, not just spawning more work in parallel.
- `synchronized`, locks, atomics, and concurrent collections exist because shared mutable state is hard to manage correctly.
- `volatile` provides visibility, not full atomicity.
- The Java Memory Model explains why synchronization rules matter for correctness across threads.
- Garbage collection removes manual memory management, but memory leaks and GC-related latency issues still exist.
- Streams are powerful for data transformation pipelines, but they are not automatically clearer or faster in every situation.
- Strong Java engineers combine runtime understanding with measurement rather than guessing about performance.
+346
View File
@@ -0,0 +1,346 @@
# File 5: Real-world Java Engineering
By the time you reach this stage, Java should no longer feel like a collection of disconnected language features. The real question becomes: how do these features come together in production systems?
This file focuses on practical Java engineering in backend services. It connects language fundamentals, OOP, APIs, concurrency, and system design intuition to the kinds of systems teams actually build and operate.
## Building Backend Services in Java
### Intuition
A backend service receives requests or events, executes business logic, talks to data stores or external systems, and returns results. Java is a strong fit for this because it combines:
- mature web frameworks
- good concurrency support
- strong type systems for large codebases
- stable operational tooling on the JVM
- rich ecosystems for persistence, messaging, validation, security, and observability
### Typical Request Flow
```mermaid
flowchart LR
A[Client] --> B[HTTP Controller]
B --> C[Service Layer]
C --> D[Repository]
C --> E[External API Client]
C --> F[Message Publisher]
D --> G[(Database)]
E --> H[Payment or Identity Provider]
F --> I[(Message Broker)]
```
### Why This Structure Is Common
- controllers handle transport details such as HTTP mapping and status codes
- services coordinate business workflows
- repositories isolate persistence concerns
- external clients isolate integrations with other services
- publishers decouple asynchronous work through queues or topics
This separation makes testing easier and changes safer.
## The Spring Ecosystem at a High Level
Spring is the dominant Java backend ecosystem in many organizations, so you need a high-level mental model even if you are not yet learning every annotation.
### Spring Framework
Spring Framework provides the core programming model:
- dependency injection
- bean lifecycle management
- AOP support
- transaction support
- web and data integration
### Spring Boot
Spring Boot is the opinionated layer that makes Spring applications faster to start and easier to run.
It gives you:
- auto-configuration
- starter dependencies
- embedded servers
- conventions for configuration and packaging
- production-oriented tooling such as Actuator
### Why Teams Use It
- less boilerplate for common backend setups
- consistent structure across services
- mature ecosystem support
- easy packaging and deployment for APIs and internal tools
### What to Understand First
At a high level, Spring Boot helps wire together objects and infrastructure so your application code can focus on business behavior.
That means a controller might receive a request, call a service bean, which depends on a repository bean, which depends on database infrastructure configured automatically by the framework.
### Startup Mental Model
```mermaid
flowchart TD
A[main method] --> B[SpringApplication.run]
B --> C[Create ApplicationContext]
C --> D[Component Scan]
C --> E[Auto-Configuration]
D --> F[Beans Created]
E --> F
F --> G[Embedded Server Starts]
G --> H[Application Ready]
```
This matters because when something breaks in a real service, you need to understand whether it is your code, dependency injection wiring, configuration, or auto-configuration behavior.
## REST API Concepts in Java
Many Java services expose REST-style HTTP APIs.
### Core Concepts
- resources are represented over HTTP
- endpoints typically map to nouns such as `/orders` or `/customers`
- standard HTTP verbs map to common actions like create, read, update, and delete
- status codes communicate outcome
- request and response bodies are usually JSON
### Example Workflow
An order API might support:
- `POST /orders` to create an order
- `GET /orders/{id}` to fetch details
- `PATCH /orders/{id}` to update state
### What Java Brings Here
Java web frameworks help with:
- request routing
- JSON serialization and deserialization
- validation of incoming payloads
- exception mapping into HTTP responses
- security filters and authentication
- observability and tracing hooks
### Production Considerations
Good API design is not only about returning JSON.
It also includes:
- idempotency where appropriate
- versioning strategy
- validation and error message clarity
- timeout behavior for downstream calls
- pagination for large results
- authentication and authorization boundaries
### Common Pitfall
Beginners often put all business logic directly in controllers. That leads to code that is difficult to test and reuse. Controllers should stay thin. They translate transport concerns and delegate real work to the service layer.
## Design Patterns in Java
Design patterns are recurring solutions to common design problems. They are useful when they make code clearer, not when they are used as decoration.
### Singleton
The singleton idea means one shared instance exists for a type.
### Where It Appears in Practice
- application-wide configuration holders
- stateless shared services managed by dependency injection containers
- logging or registry-like components in some designs
### Caution
Hand-written singletons can create hidden global state, testability problems, and lifecycle issues. In modern Java backend systems, dependency injection frameworks often manage shared singleton-like components for you in a safer way.
### Factory
A factory centralizes object creation.
```java
public interface ReportExporter {
byte[] export(Report report);
}
public class ReportExporterFactory {
public ReportExporter create(String format) {
return switch (format) {
case "csv" -> new CsvReportExporter();
case "pdf" -> new PdfReportExporter();
default -> throw new IllegalArgumentException("Unsupported format: " + format);
};
}
}
```
Factories are useful when construction rules vary or when the caller should not depend on concrete types.
### Strategy
Strategy encapsulates interchangeable algorithms behind a common interface.
```java
public interface PricingStrategy {
Money calculate(Cart cart);
}
```
Useful in:
- discount logic by customer type
- routing behavior by region
- fraud checks by provider
- retry policies by integration
### Builder
Builder helps construct complex objects with many optional fields.
This is common in:
- DTO creation
- test data setup
- configuration objects
- HTTP client requests
### Observer and Event-Driven Patterns
Java systems often use event-driven design, either inside the application or across services.
Examples:
- publish an event when an order is placed
- notify inventory and analytics consumers asynchronously
- trigger email sending after the main transaction completes
This reduces tight coupling and supports scalable workflows.
## System Design Relevance
Java engineering becomes much more effective when you connect code-level decisions to system-level behavior.
### Example: Synchronous vs Asynchronous Work
Suppose an API receives an order request.
Should it do all work inline?
- charge payment
- reserve inventory
- send email
- write analytics records
Probably not. Some work belongs in the synchronous request path. Some belongs in asynchronous messaging.
Java and the surrounding ecosystem make both styles possible.
### Example: Layered Architecture
Separating controllers, services, repositories, and integrations is not just style. It supports:
- easier testing
- clearer ownership of business rules
- safer refactoring
- better operational debugging
### Example: Resilience
If your Java service depends on another service, you need to think about:
- timeouts
- retries
- circuit breaking
- fallbacks
- idempotency
These are system design concerns, but they show up in code through client configuration, exception handling, and workflow design.
## Production Best Practices
### Keep Business Logic Out of Framework Glue
Framework annotations and configurations are useful, but the business rules should still be understandable in plain Java classes. This makes testing and refactoring easier.
### Prefer Constructor Injection
It keeps dependencies explicit and reduces hidden magic.
### Model Failure Clearly
Do not swallow exceptions. Decide what should be retried, what should fail fast, and what should surface to callers with meaningful error information.
### Use Validation at Boundaries
Validate incoming API payloads, message payloads, and configuration values early. Invalid inputs should not travel deep into the system.
### Be Intentional About Concurrency
Do not share mutable state casually. Understand thread pools, blocking calls, and contention points.
### Observe the System
Production systems need:
- structured logs
- metrics
- traces
- health checks
- dashboards and alerts
If a service fails at 3 AM, observability is what turns confusion into diagnosis.
### Design for Change
Requirements evolve. Code that is rigid or overcoupled becomes expensive quickly. Good Java systems isolate change behind interfaces, composition, configuration, and focused modules.
### Think About Data Boundaries
Be careful about leaking persistence models directly into API responses. Domain models, DTOs, and storage schemas often evolve at different speeds.
## How a Real Java Service Evolves
An actual production Java service often grows through stages:
1. start with a few endpoints and straightforward business logic
2. add persistence, validation, and external integrations
3. introduce asynchronous processing for slow side effects
4. add caching, concurrency controls, and observability
5. harden the service with better error handling, retries, metrics, and deployment discipline
At each stage, the Java concepts from the earlier files become more important, not less.
- OOP shapes the service boundaries
- collections and exceptions shape data flow and failure handling
- concurrency affects throughput and safety
- framework knowledge affects startup, deployment, and maintainability
## A Senior Engineer's Mental Checklist
When reading or designing Java backend code, ask:
- where does the request enter the system?
- where do business rules live?
- what dependencies does this component have?
- what happens when a downstream dependency is slow or unavailable?
- what state is shared and how is it protected?
- what gets logged, measured, and traced?
- how easy is this code to test without the whole framework running?
These questions are often more valuable than memorizing another annotation.
## Key Takeaways
- Real-world Java engineering is about combining language fundamentals with runtime, framework, and system design thinking.
- Backend Java services usually separate transport, business logic, persistence, and external integrations into focused layers.
- The Spring ecosystem matters because it is a common way Java teams wire objects, configure infrastructure, and ship services.
- REST APIs in Java require careful thinking about validation, status codes, timeouts, idempotency, and service boundaries.
- Design patterns such as factory, strategy, and builder are useful when they clarify construction and variation, not when they add ceremony.
- Production quality comes from explicit dependencies, clear failure handling, observability, safe concurrency, and code organized for change.