Intermediate
Ever wondered how enterprise systems handle millions of users without crashing? This tutorial bridges the gap between basic understanding and professional development. You’ll learn design patterns, advanced concurrency, performance optimization, and integration with databases and build tools.
What You’ll Achieve
By the end of this tutorial, you will:
- ✅ Understand and apply common design patterns
- ✅ Apply SOLID principles to real code
- ✅ Work with multiple threads safely
- ✅ Profile and optimize Java applications
- ✅ Use Maven and Gradle for project management
- ✅ Connect to databases with JDBC
- ✅ Implement security best practices
- ✅ Use advanced collections techniques
- ✅ Design and architect medium-scale systems
- ✅ Know what to learn for expert mastery
Prerequisites
- Completed Beginner’s Guide to Java - Comfortable with OOP, collections, testing
- Or equivalent Java experience with classes, inheritance, interfaces
Learning Path Overview
This tutorial covers 60-85% of Java knowledge needed for production development. It’s focused on practical, real-world techniques.
We’ll progress through:
- Design Patterns - Reusable solutions to common problems
- SOLID Principles in Practice - Applying to real code
- Advanced Concurrency - Multiple threads and synchronization
- Build Tools - Maven and Gradle for project management
- Performance and Profiling - Making Java fast
- Database Integration - JDBC and data persistence
- Advanced Collections - Beyond the basics
- Security Best Practices - Protecting applications
- System Design - Architecting larger systems
- Production Patterns - Real-world techniques
Part 1: Design Patterns
Design patterns are proven solutions to common problems. They make code more reusable, maintainable, and understandable.
1.1 Singleton Pattern
Singleton ensures a class has only one instance.
Example: Singleton
// Thread-safe singleton using eager initialization
public class DatabaseConnection {
private static final DatabaseConnection instance = new DatabaseConnection();
private DatabaseConnection() {
System.out.println("DatabaseConnection initialized");
}
public static DatabaseConnection getInstance() {
return instance;
}
public void connect(String url) {
System.out.println("Connecting to: " + url);
}
}
// Usage
public class SingletonDemo {
public static void main(String[] args) {
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();
System.out.println(db1 == db2); // true - same instance
}
}Use Singleton for:
- Database connections
- Logger instances
- Configuration managers
- Thread pools
1.2 Factory Pattern
Factory creates objects without exposing creation logic.
Example: Factory Pattern
// Interface
public interface DataSource {
void connect();
void query(String sql);
}
// Implementations
public class MySQLDataSource implements DataSource {
@Override
public void connect() {
System.out.println("Connecting to MySQL");
}
@Override
public void query(String sql) {
System.out.println("MySQL Query: " + sql);
}
}
public class PostgresDataSource implements DataSource {
@Override
public void connect() {
System.out.println("Connecting to PostgreSQL");
}
@Override
public void query(String sql) {
System.out.println("PostgreSQL Query: " + sql);
}
}
// Factory
public class DataSourceFactory {
public static DataSource create(String type) {
switch (type.toLowerCase()) {
case "mysql":
return new MySQLDataSource();
case "postgres":
return new PostgresDataSource();
default:
throw new IllegalArgumentException("Unknown type: " + type);
}
}
}
// Usage
public class FactoryDemo {
public static void main(String[] args) {
DataSource mysql = DataSourceFactory.create("mysql");
DataSource postgres = DataSourceFactory.create("postgres");
mysql.connect();
mysql.query("SELECT * FROM users");
}
}Use Factory for:
- Creating objects based on configuration
- Abstracting creation logic
- Supporting multiple implementations
1.3 Observer Pattern
Observer notifies multiple objects about state changes.
Example: Observer Pattern
import java.util.*;
// Observer interface
public interface Observer {
void update(String event);
}
// Subject (observable)
public class EventBus {
private List<Observer> observers = new ArrayList<>();
public void subscribe(Observer observer) {
observers.add(observer);
}
public void unsubscribe(Observer observer) {
observers.remove(observer);
}
public void publish(String event) {
for (Observer observer : observers) {
observer.update(event);
}
}
}
// Concrete observers
public class EmailNotifier implements Observer {
@Override
public void update(String event) {
System.out.println("Email: " + event);
}
}
public class LogNotifier implements Observer {
@Override
public void update(String event) {
System.out.println("Log: " + event);
}
}
// Usage
public class ObserverDemo {
public static void main(String[] args) {
EventBus eventBus = new EventBus();
eventBus.subscribe(new EmailNotifier());
eventBus.subscribe(new LogNotifier());
eventBus.publish("User registered");
// Output:
// Email: User registered
// Log: User registered
}
}1.4 Strategy Pattern
Strategy encapsulates algorithms as interchangeable objects.
Example: Strategy Pattern
// Strategy interface
public interface PaymentStrategy {
void pay(double amount);
}
// Concrete strategies
public class CreditCardPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paying $" + amount + " with credit card");
}
}
public class PayPalPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paying $" + amount + " via PayPal");
}
}
public class CryptocurrencyPayment implements PaymentStrategy {
@Override
public void pay(double amount) {
System.out.println("Paying $" + amount + " with cryptocurrency");
}
}
// Context
public class ShoppingCart {
private PaymentStrategy strategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.strategy = strategy;
}
public void checkout(double total) {
if (strategy == null) {
throw new IllegalStateException("Payment strategy not set");
}
strategy.pay(total);
}
}
// Usage
public class StrategyDemo {
public static void main(String[] args) {
ShoppingCart cart = new ShoppingCart();
cart.setPaymentStrategy(new CreditCardPayment());
cart.checkout(100.00);
cart.setPaymentStrategy(new PayPalPayment());
cart.checkout(50.00);
}
}Other Useful Patterns:
- Decorator - Add behavior to objects
- Adapter - Convert interfaces
- Builder - Construct complex objects
- Template Method - Define algorithm skeleton
Part 2: SOLID Principles in Practice
2.1 Dependency Injection
Dependency Injection provides dependencies rather than creating them internally.
Bad: Tight Coupling
public class OrderService {
private EmailService emailService = new EmailService(); // Tightly coupled!
public void placeOrder(Order order) {
// Process order
emailService.sendConfirmation(order);
}
}Good: Dependency Injection
// Service interface
public interface NotificationService {
void sendConfirmation(Order order);
}
// Implementations
public class EmailNotificationService implements NotificationService {
@Override
public void sendConfirmation(Order order) {
System.out.println("Email sent");
}
}
public class SMSNotificationService implements NotificationService {
@Override
public void sendConfirmation(Order order) {
System.out.println("SMS sent");
}
}
// Service with injected dependency
public class OrderService {
private NotificationService notificationService;
// Constructor injection
public OrderService(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void placeOrder(Order order) {
// Process order
notificationService.sendConfirmation(order);
}
}
// Usage
public class DIDemo {
public static void main(String[] args) {
NotificationService emailService = new EmailNotificationService();
OrderService orderService = new OrderService(emailService);
orderService.placeOrder(new Order(123));
}
}Benefits:
- Easy to test (inject mock services)
- Loosely coupled
- Easy to swap implementations
2.2 Interface Segregation
Don’t force clients to depend on methods they don’t use.
Bad: Fat Interface
public interface Worker {
void work();
void eat();
void manage();
}
// Robot shouldn't implement eat()
public class Robot implements Worker {
@Override
public void work() { /* code */ }
@Override
public void eat() { /* doesn't apply */ }
@Override
public void manage() { /* doesn't apply */ }
}Good: Segregated Interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public interface Manageable {
void manage();
}
public class Robot implements Workable {
@Override
public void work() { /* code */ }
}
public class Human implements Workable, Eatable, Manageable {
@Override
public void work() { /* code */ }
@Override
public void eat() { /* code */ }
@Override
public void manage() { /* code */ }
}Part 2B: Streams API (Visual Guide)
Understanding Stream Pipelines
Streams process data through a pipeline of operations, similar to an assembly line:
graph TD
A["📊 Source<br/>List of numbers<br/>1,2,3,4,5,6,7,8,9,10"] -->|filter<br/>Keep evens| B["2,4,6,8,10"]
B -->|map<br/>Square each| C["4,16,36,64,100"]
C -->|reduce<br/>Sum all| D["✅ Result<br/>220"]
style A fill:#0173B2,stroke:#000000,color:#FFFFFF
style B fill:#DE8F05
style C fill:#DE8F05
style D fill:#029E73,stroke:#000000,color:#FFFFFF
Each operation transforms the data and passes it to the next stage. This is immensely powerful for data processing.
Part 3: Advanced Concurrency
3.1 Threads and Synchronization
Concurrency allows multiple tasks to execute simultaneously.
Example: Creating Threads
// Method 1: Extend Thread
public class CounterThread extends Thread {
private int id;
public CounterThread(int id) {
this.id = id;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + id + ": " + i);
try {
Thread.sleep(1000); // Sleep 1 second
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
// Method 2: Implement Runnable (preferred)
public class CounterRunnable implements Runnable {
private int id;
public CounterRunnable(int id) {
this.id = id;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread " + id + ": " + i);
}
}
}
// Usage
public class ThreadDemo {
public static void main(String[] args) {
// Method 2: More flexible
Thread t1 = new Thread(new CounterRunnable(1));
Thread t2 = new Thread(new CounterRunnable(2));
t1.start();
t2.start();
try {
t1.join(); // Wait for t1 to finish
t2.join(); // Wait for t2 to finish
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("All threads done");
}
}3.2 Thread Safety with Synchronized
⚠️ Thread Safety Warning
Threads are powerful but dangerous. Consider this seemingly innocent code:
class Counter {
private int count = 0;
public void increment() {
count++; // NOT thread-safe!
}
}If two threads call increment() simultaneously, you might lose counts. This is a race condition - a common source of production bugs.
Rule: Only use threads when you understand synchronization. For most cases, use higher-level abstractions like ExecutorService or CompletableFuture.
Synchronization prevents race conditions when multiple threads access shared data.
Example: Synchronization
// NOT thread-safe
public class UnsafeCounter {
private int count = 0;
public void increment() {
count++; // NOT atomic! Race condition possible
}
public int getCount() {
return count;
}
}
// Thread-safe with synchronized
public class SafeCounter {
private int count = 0;
// Synchronized method
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
// Better: Synchronized block
public class OptimizedCounter {
private int count = 0;
public void increment() {
// Only lock what's necessary
synchronized (this) {
count++;
}
}
public int getCount() {
synchronized (this) {
return count;
}
}
}
// Best: Use atomic classes
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}3.3 Concurrent Collections
Concurrent collections are thread-safe without explicit synchronization.
Example: Concurrent Collections
import java.util.concurrent.*;
public class ConcurrentCollectionsDemo {
public static void main(String[] args) throws InterruptedException {
// ConcurrentHashMap - thread-safe without locking entire map
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// Multiple threads can put/get safely
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
final int id = i;
executor.submit(() -> {
map.put("key" + id, id * 100);
});
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("Size: " + map.size());
// BlockingQueue - safe queue for producer-consumer
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// Producer thread
new Thread(() -> {
try {
queue.put("item1");
queue.put("item2");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
// Consumer thread
new Thread(() -> {
try {
System.out.println(queue.take()); // Waits if empty
System.out.println(queue.take());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}Part 4: Build Tools
4.1 Maven Basics
Maven manages dependencies and builds.
Example: Maven POM
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>my-app</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- JUnit 5 for testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
<!-- JSON processing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>
</project>Common Commands:
mvn clean- Remove build artifactsmvn compile- Compile source codemvn test- Run testsmvn package- Create JARmvn install- Install to local repository
4.2 Gradle Basics
Gradle is a modern build tool (alternative to Maven).
Example: Gradle Build File
plugins {
id 'java'
}
java {
sourceCompatibility = '21'
targetCompatibility = '21'
}
repositories {
mavenCentral()
}
dependencies {
// JUnit 5
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.0'
// JSON
implementation 'com.google.code.gson:gson:2.10.1'
}
tasks.named('test') {
useJUnitPlatform()
}Common Commands:
gradle build- Build projectgradle test- Run testsgradle clean- Remove build filesgradle jar- Create JAR
Part 5: Performance and Profiling
5.1 Performance Best Practices
String Operations
// Bad: Creates many String objects
String result = "";
for (int i = 0; i < 10000; i++) {
result += "item" + i + ","; // Inefficient!
}
// Good: Use StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append("item").append(i).append(",");
}
String result = sb.toString();Collection Sizing
// Bad: ArrayList grows repeatedly
List<String> items = new ArrayList<>();
for (int i = 0; i < 100000; i++) {
items.add("item " + i); // Reallocates internally
}
// Good: Size ArrayList upfront
List<String> items = new ArrayList<>(100000);
for (int i = 0; i < 100000; i++) {
items.add("item " + i);
}Caching
// Bad: Recalculate repeatedly
public int expensiveOperation(int n) {
int result = 0;
for (int i = 0; i < 1000000; i++) {
result += heavyCalculation(n);
}
return result;
}
// Good: Cache results
private Map<Integer, Integer> cache = new HashMap<>();
public int expensiveOperation(int n) {
if (cache.containsKey(n)) {
return cache.get(n);
}
int result = 0;
for (int i = 0; i < 1000000; i++) {
result += heavyCalculation(n);
}
cache.put(n, result);
return result;
}5.2 Profiling with JVisualVM
JVisualVM comes with Java and helps identify performance bottlenecks.
java -jar myapp.jar
jvisualvmPart 6: Database Integration with JDBC
6.1 Basic JDBC
JDBC connects Java to databases.
Example: JDBC Connection
import java.sql.*;
public class JDBCExample {
public static void main(String[] args) {
String url = "jdbc:mysql://localhost:3306/mydb";
String user = "root";
String password = "password";
try {
// Note: Class.forName() is optional for JDBC 4.0+ drivers (auto-loaded)
// Only needed for legacy pre-JDBC 4.0 drivers
Class.forName("com.mysql.cj.jdbc.Driver");
// Get connection (driver auto-loaded from classpath for JDBC 4.0+)
Connection conn = DriverManager.getConnection(url, user, password);
// Create statement
Statement stmt = conn.createStatement();
// Execute query
String sql = "SELECT * FROM users WHERE age > 18";
ResultSet rs = stmt.executeQuery(sql);
// Process results
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
System.out.println(id + ": " + name + " (" + age + ")");
}
// Close resources
rs.close();
stmt.close();
conn.close();
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
}6.2 Prepared Statements (SQL Injection Prevention)
// Bad: SQL Injection vulnerability
String sql = "SELECT * FROM users WHERE username = '" + userInput + "'";
// Good: Use PreparedStatement
String sql = "SELECT * FROM users WHERE username = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userInput); // Safely bind parameter
ResultSet rs = pstmt.executeQuery();Part 7: Security Best Practices
7.1 Password Handling
import java.security.MessageDigest;
import java.util.Arrays;
public class PasswordSecurity {
// Hash password with salt
public static String hashPassword(String password, byte[] salt) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(salt);
byte[] hashedPassword = md.digest(password.getBytes());
return bytesToHex(hashedPassword);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}7.2 Input Validation
public class InputValidation {
public static boolean isValidEmail(String email) {
return email.matches("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$");
}
public static int parseIntSafely(String value, int defaultValue) {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
}Part 8: System Design Principles
8.1 Scalability
// Use connection pooling for database
import org.apache.commons.dbcp2.BasicDataSource;
public class ConnectionPool {
private static BasicDataSource ds = new BasicDataSource();
static {
ds.setUrl("jdbc:mysql://localhost/mydb");
ds.setUsername("root");
ds.setPassword("password");
ds.setInitialSize(5);
ds.setMaxTotal(10);
}
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
}8.2 Caching Layer
public class CachingUserRepository {
private Map<Integer, User> cache = new ConcurrentHashMap<>();
private UserRepository repository;
public User getUser(int id) {
// Check cache first
if (cache.containsKey(id)) {
return cache.get(id);
}
// Fetch from database
User user = repository.findById(id);
// Store in cache
cache.put(id, user);
return user;
}
}Part 9: Production Patterns
9.1 Logging
Use proper logging instead of System.out.println().
import java.util.logging.Logger;
public class UserService {
private static final Logger logger = Logger.getLogger(UserService.class.getName());
public void createUser(String name) {
try {
logger.info("Creating user: " + name);
// Create user
logger.info("User created successfully");
} catch (Exception e) {
logger.severe("Failed to create user: " + e.getMessage());
}
}
}9.2 Configuration Management
import java.io.FileInputStream;
import java.util.Properties;
public class Config {
private static Properties props = new Properties();
static {
try {
props.load(new FileInputStream("config.properties"));
} catch (Exception e) {
e.printStackTrace();
}
}
public static String get(String key) {
return props.getProperty(key);
}
public static String get(String key, String defaultValue) {
return props.getProperty(key, defaultValue);
}
}
// config.properties
// database.url=jdbc:mysql://localhost/mydb
// database.user=root
// logging.level=INFORelated Content
Previous Tutorials:
- Beginner Java - Java fundamentals and OOP
- Quick Start - Quick overview
Next Steps:
- Advanced Java - JVM internals and expert topics
How-To Guides:
- How to Implement Singleton Pattern - Singleton best practices
- How to Implement Factory Pattern - Factory pattern guide
- How to Use Dependency Injection - DI frameworks
- How to Optimize Java Performance - Performance tuning
- How to Debug Concurrency Issues - Threading problems
- How to Work with JDBC - Database connectivity
- How to Manage Dependencies with Maven - Build automation
Cookbook:
- Java Cookbook - Production recipes
Explanations:
- Best Practices - Professional standards
- Anti-Patterns - Common pitfalls
Reference:
- Java Cheat Sheet - Quick reference
What to Learn Next
Excellent progress! You’ve covered 60-85% of Java knowledge for professional development.
Next: Advanced Java
Continue with Advanced Java for:
- JVM internals and architecture
- Garbage collection tuning
- Advanced concurrency patterns
- Reflection and annotations
- Bytecode analysis
For Day-to-Day Solutions
Use Java Cookbook for:
- Quick patterns and recipes
- Common problems and solutions
- Copy-paste code snippets