Design PatternsJavaSoftware ArchitectureBest PracticesOOP
Java Design Patterns: Essential Patterns for Enterprise Applications
January 7, 2025•18 min read•Design Patterns
# Java Design Patterns: Essential Patterns for Enterprise Applications
Design patterns are proven solutions to recurring design problems in software development. Understanding and applying these patterns correctly can significantly improve code quality, maintainability, and team communication. This guide covers essential patterns for Java enterprise applications.
## Creational Patterns
Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.
### Singleton Pattern
Ensures a class has only one instance and provides global access to it:
```java
// Thread-safe Singleton with enum (recommended)
public enum DatabaseConnection {
INSTANCE;
private Connection connection;
DatabaseConnection() {
// Initialize connection
try {
this.connection = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/mydb",
"username",
"password"
);
} catch (SQLException e) {
throw new RuntimeException("Failed to create database connection", e);
}
}
public Connection getConnection() {
return connection;
}
public void executeQuery(String sql) {
// Execute query using connection
}
}
// Thread-safe Singleton with double-checked locking
public class ConfigurationManager {
private static volatile ConfigurationManager instance;
private final Properties properties;
private ConfigurationManager() {
properties = new Properties();
loadConfiguration();
}
public static ConfigurationManager getInstance() {
if (instance == null) {
synchronized (ConfigurationManager.class) {
if (instance == null) {
instance = new ConfigurationManager();
}
}
}
return instance;
}
private void loadConfiguration() {
try (InputStream input = getClass().getResourceAsStream("/application.properties")) {
properties.load(input);
} catch (IOException e) {
throw new RuntimeException("Failed to load configuration", e);
}
}
public String getProperty(String key) {
return properties.getProperty(key);
}
public String getProperty(String key, String defaultValue) {
return properties.getProperty(key, defaultValue);
}
}
// Usage
DatabaseConnection.INSTANCE.executeQuery("SELECT * FROM users");
ConfigurationManager config = ConfigurationManager.getInstance();
String dbUrl = config.getProperty("database.url");
```
### Factory Pattern
Creates objects without specifying their exact classes:
```java
// Product interface
public interface PaymentProcessor {
void processPayment(BigDecimal amount, String currency);
boolean validatePayment(PaymentDetails details);
PaymentResult getPaymentStatus(String transactionId);
}
// Concrete products
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(BigDecimal amount, String currency) {
System.out.println("Processing credit card payment: " + amount + " " + currency);
// Credit card specific logic
}
@Override
public boolean validatePayment(PaymentDetails details) {
// Validate credit card details
return details.getCardNumber() != null && details.getCvv() != null;
}
@Override
public PaymentResult getPaymentStatus(String transactionId) {
// Get credit card payment status
return new PaymentResult(transactionId, PaymentStatus.COMPLETED);
}
}
public class PayPalProcessor implements PaymentProcessor {
@Override
public void processPayment(BigDecimal amount, String currency) {
System.out.println("Processing PayPal payment: " + amount + " " + currency);
// PayPal specific logic
}
@Override
public boolean validatePayment(PaymentDetails details) {
// Validate PayPal details
return details.getEmail() != null;
}
@Override
public PaymentResult getPaymentStatus(String transactionId) {
// Get PayPal payment status
return new PaymentResult(transactionId, PaymentStatus.PENDING);
}
}
public class BankTransferProcessor implements PaymentProcessor {
@Override
public void processPayment(BigDecimal amount, String currency) {
System.out.println("Processing bank transfer: " + amount + " " + currency);
// Bank transfer specific logic
}
@Override
public boolean validatePayment(PaymentDetails details) {
// Validate bank details
return details.getAccountNumber() != null && details.getRoutingNumber() != null;
}
@Override
public PaymentResult getPaymentStatus(String transactionId) {
// Get bank transfer status
return new PaymentResult(transactionId, PaymentStatus.PROCESSING);
}
}
// Factory
public class PaymentProcessorFactory {
public static PaymentProcessor createProcessor(PaymentType type) {
return switch (type) {
case CREDIT_CARD -> new CreditCardProcessor();
case PAYPAL -> new PayPalProcessor();
case BANK_TRANSFER -> new BankTransferProcessor();
default -> throw new IllegalArgumentException("Unsupported payment type: " + type);
};
}
}
// Abstract Factory for different regions
public interface PaymentProcessorAbstractFactory {
PaymentProcessor createCreditCardProcessor();
PaymentProcessor createDigitalWalletProcessor();
PaymentProcessor createBankTransferProcessor();
}
public class USPaymentProcessorFactory implements PaymentProcessorAbstractFactory {
@Override
public PaymentProcessor createCreditCardProcessor() {
return new USCreditCardProcessor();
}
@Override
public PaymentProcessor createDigitalWalletProcessor() {
return new PayPalProcessor();
}
@Override
public PaymentProcessor createBankTransferProcessor() {
return new ACHProcessor();
}
}
public class EUPaymentProcessorFactory implements PaymentProcessorAbstractFactory {
@Override
public PaymentProcessor createCreditCardProcessor() {
return new EUCreditCardProcessor();
}
@Override
public PaymentProcessor createDigitalWalletProcessor() {
return new PayPalProcessor();
}
@Override
public PaymentProcessor createBankTransferProcessor() {
return new SEPAProcessor();
}
}
// Usage
PaymentProcessor processor = PaymentProcessorFactory.createProcessor(PaymentType.CREDIT_CARD);
processor.processPayment(new BigDecimal("100.00"), "USD");
PaymentProcessorAbstractFactory factory = new USPaymentProcessorFactory();
PaymentProcessor creditCardProcessor = factory.createCreditCardProcessor();
```
### Builder Pattern
Constructs complex objects step by step:
```java
// Product class
public class User {
private final String firstName;
private final String lastName;
private final String email;
private final String phone;
private final Address address;
private final List<String> roles;
private final Map<String, String> preferences;
private final boolean active;
private final LocalDateTime createdDate;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.email = builder.email;
this.phone = builder.phone;
this.address = builder.address;
this.roles = Collections.unmodifiableList(new ArrayList<>(builder.roles));
this.preferences = Collections.unmodifiableMap(new HashMap<>(builder.preferences));
this.active = builder.active;
this.createdDate = builder.createdDate;
}
// Getters
public String getFirstName() { return firstName; }
public String getLastName() { return lastName; }
public String getEmail() { return email; }
public String getPhone() { return phone; }
public Address getAddress() { return address; }
public List<String> getRoles() { return roles; }
public Map<String, String> getPreferences() { return preferences; }
public boolean isActive() { return active; }
public LocalDateTime getCreatedDate() { return createdDate; }
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String firstName;
private String lastName;
private String email;
private String phone;
private Address address;
private List<String> roles = new ArrayList<>();
private Map<String, String> preferences = new HashMap<>();
private boolean active = true;
private LocalDateTime createdDate = LocalDateTime.now();
public Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder address(Address address) {
this.address = address;
return this;
}
public Builder addRole(String role) {
this.roles.add(role);
return this;
}
public Builder roles(List<String> roles) {
this.roles = new ArrayList<>(roles);
return this;
}
public Builder addPreference(String key, String value) {
this.preferences.put(key, value);
return this;
}
public Builder preferences(Map<String, String> preferences) {
this.preferences = new HashMap<>(preferences);
return this;
}
public Builder active(boolean active) {
this.active = active;
return this;
}
public Builder createdDate(LocalDateTime createdDate) {
this.createdDate = createdDate;
return this;
}
public User build() {
validate();
return new User(this);
}
private void validate() {
if (firstName == null || firstName.trim().isEmpty()) {
throw new IllegalArgumentException("First name is required");
}
if (lastName == null || lastName.trim().isEmpty()) {
throw new IllegalArgumentException("Last name is required");
}
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Valid email is required");
}
}
}
}
// Usage
User user = User.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@example.com")
.phone("+1-555-123-4567")
.address(new Address("123 Main St", "Anytown", "ST", "12345"))
.addRole("USER")
.addRole("ADMIN")
.addPreference("theme", "dark")
.addPreference("language", "en")
.active(true)
.build();
```
## Structural Patterns
Structural patterns deal with object composition and relationships between objects.
### Adapter Pattern
Allows incompatible interfaces to work together:
```java
// Target interface (what client expects)
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// Adaptee (existing class with incompatible interface)
public class AdvancedMediaPlayer {
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
// Adapter
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedPlayer;
public MediaAdapter(String audioType) {
if ("vlc".equalsIgnoreCase(audioType) || "mp4".equalsIgnoreCase(audioType)) {
advancedPlayer = new AdvancedMediaPlayer();
}
}
@Override
public void play(String audioType, String fileName) {
if ("vlc".equalsIgnoreCase(audioType)) {
advancedPlayer.playVlc(fileName);
} else if ("mp4".equalsIgnoreCase(audioType)) {
advancedPlayer.playMp4(fileName);
}
}
}
// Client
public class AudioPlayer implements MediaPlayer {
private MediaAdapter mediaAdapter;
@Override
public void play(String audioType, String fileName) {
if ("mp3".equalsIgnoreCase(audioType)) {
System.out.println("Playing mp3 file: " + fileName);
} else if ("vlc".equalsIgnoreCase(audioType) || "mp4".equalsIgnoreCase(audioType)) {
mediaAdapter = new MediaAdapter(audioType);
mediaAdapter.play(audioType, fileName);
} else {
System.out.println("Invalid media. " + audioType + " format not supported");
}
}
}
// Real-world example: Database adapter
public interface DatabaseConnection {
void connect();
void disconnect();
ResultSet executeQuery(String query);
int executeUpdate(String query);
}
public class MySQLConnection {
public void establishConnection() { /* MySQL specific */ }
public void closeConnection() { /* MySQL specific */ }
public MySQLResultSet runQuery(String sql) { /* MySQL specific */ }
public int runUpdate(String sql) { /* MySQL specific */ }
}
public class PostgreSQLConnection {
public void openConnection() { /* PostgreSQL specific */ }
public void terminateConnection() { /* PostgreSQL specific */ }
public PostgreSQLResultSet performQuery(String sql) { /* PostgreSQL specific */ }
public int performUpdate(String sql) { /* PostgreSQL specific */ }
}
public class MySQLAdapter implements DatabaseConnection {
private MySQLConnection mysqlConnection;
public MySQLAdapter(MySQLConnection mysqlConnection) {
this.mysqlConnection = mysqlConnection;
}
@Override
public void connect() {
mysqlConnection.establishConnection();
}
@Override
public void disconnect() {
mysqlConnection.closeConnection();
}
@Override
public ResultSet executeQuery(String query) {
MySQLResultSet mysqlResult = mysqlConnection.runQuery(query);
return new ResultSetAdapter(mysqlResult);
}
@Override
public int executeUpdate(String query) {
return mysqlConnection.runUpdate(query);
}
}
```
### Decorator Pattern
Adds new functionality to objects dynamically without altering their structure:
```java
// Component interface
public interface Coffee {
String getDescription();
double getCost();
}
// Concrete component
public class SimpleCoffee implements Coffee {
@Override
public String getDescription() {
return "Simple coffee";
}
@Override
public double getCost() {
return 2.0;
}
}
// Base decorator
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// Concrete decorators
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", milk";
}
@Override
public double getCost() {
return coffee.getCost() + 0.5;
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", sugar";
}
@Override
public double getCost() {
return coffee.getCost() + 0.2;
}
}
public class WhipDecorator extends CoffeeDecorator {
public WhipDecorator(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", whip";
}
@Override
public double getCost() {
return coffee.getCost() + 0.7;
}
}
// Usage
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription() + " $" + coffee.getCost());
// Real-world example: Stream decorators
public interface DataSource {
void writeData(String data);
String readData();
}
public class FileDataSource implements DataSource {
private String filename;
public FileDataSource(String filename) {
this.filename = filename;
}
@Override
public void writeData(String data) {
// Write data to file
try (FileWriter writer = new FileWriter(filename)) {
writer.write(data);
} catch (IOException e) {
throw new RuntimeException("Error writing to file", e);
}
}
@Override
public String readData() {
// Read data from file
try {
return Files.readString(Paths.get(filename));
} catch (IOException e) {
throw new RuntimeException("Error reading from file", e);
}
}
}
public abstract class DataSourceDecorator implements DataSource {
protected DataSource wrappee;
public DataSourceDecorator(DataSource source) {
this.wrappee = source;
}
@Override
public void writeData(String data) {
wrappee.writeData(data);
}
@Override
public String readData() {
return wrappee.readData();
}
}
public class EncryptionDecorator extends DataSourceDecorator {
public EncryptionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(encrypt(data));
}
@Override
public String readData() {
return decrypt(super.readData());
}
private String encrypt(String data) {
// Simple encryption (in real world, use proper encryption)
return Base64.getEncoder().encodeToString(data.getBytes());
}
private String decrypt(String data) {
return new String(Base64.getDecoder().decode(data));
}
}
public class CompressionDecorator extends DataSourceDecorator {
public CompressionDecorator(DataSource source) {
super(source);
}
@Override
public void writeData(String data) {
super.writeData(compress(data));
}
@Override
public String readData() {
return decompress(super.readData());
}
private String compress(String data) {
// Simple compression simulation
return data.replaceAll("\s+", " ");
}
private String decompress(String data) {
// Simple decompression simulation
return data;
}
}
// Usage
DataSource source = new FileDataSource("data.txt");
source = new EncryptionDecorator(source);
source = new CompressionDecorator(source);
source.writeData("Hello World! This is a test message.");
String data = source.readData();
```
### Facade Pattern
Provides a simplified interface to a complex subsystem:
```java
// Complex subsystem classes
public class CPU {
public void freeze() {
System.out.println("CPU: Freezing processor");
}
public void jump(long position) {
System.out.println("CPU: Jumping to position " + position);
}
public void execute() {
System.out.println("CPU: Executing instructions");
}
}
public class Memory {
public void load(long position, String data) {
System.out.println("Memory: Loading data '" + data + "' at position " + position);
}
}
public class HardDrive {
public String read(long lba, int size) {
System.out.println("HardDrive: Reading " + size + " bytes from LBA " + lba);
return "boot data";
}
}
// Facade
public class ComputerFacade {
private CPU cpu;
private Memory memory;
private HardDrive hardDrive;
public ComputerFacade() {
this.cpu = new CPU();
this.memory = new Memory();
this.hardDrive = new HardDrive();
}
public void start() {
System.out.println("Starting computer...");
cpu.freeze();
memory.load(0, hardDrive.read(0, 1024));
cpu.jump(0);
cpu.execute();
System.out.println("Computer started successfully!");
}
}
// Real-world example: Order processing facade
public class InventoryService {
public boolean checkAvailability(String productId, int quantity) {
System.out.println("Checking inventory for " + productId + " quantity: " + quantity);
return true;
}
public void reserveItems(String productId, int quantity) {
System.out.println("Reserving " + quantity + " items of " + productId);
}
}
public class PaymentService {
public boolean processPayment(String customerId, BigDecimal amount) {
System.out.println("Processing payment of $" + amount + " for customer " + customerId);
return true;
}
}
public class ShippingService {
public String scheduleShipping(String customerId, String address) {
System.out.println("Scheduling shipping to " + address + " for customer " + customerId);
return "SHIP123456";
}
}
public class NotificationService {
public void sendOrderConfirmation(String customerId, String orderId) {
System.out.println("Sending order confirmation for order " + orderId + " to customer " + customerId);
}
}
// Facade for order processing
public class OrderProcessingFacade {
private InventoryService inventoryService;
private PaymentService paymentService;
private ShippingService shippingService;
private NotificationService notificationService;
public OrderProcessingFacade() {
this.inventoryService = new InventoryService();
this.paymentService = new PaymentService();
this.shippingService = new ShippingService();
this.notificationService = new NotificationService();
}
public OrderResult processOrder(OrderRequest orderRequest) {
try {
// Step 1: Check inventory
if (!inventoryService.checkAvailability(orderRequest.getProductId(), orderRequest.getQuantity())) {
return OrderResult.failure("Product not available");
}
// Step 2: Process payment
if (!paymentService.processPayment(orderRequest.getCustomerId(), orderRequest.getAmount())) {
return OrderResult.failure("Payment failed");
}
// Step 3: Reserve inventory
inventoryService.reserveItems(orderRequest.getProductId(), orderRequest.getQuantity());
// Step 4: Schedule shipping
String trackingNumber = shippingService.scheduleShipping(
orderRequest.getCustomerId(),
orderRequest.getShippingAddress()
);
// Step 5: Send confirmation
String orderId = generateOrderId();
notificationService.sendOrderConfirmation(orderRequest.getCustomerId(), orderId);
return OrderResult.success(orderId, trackingNumber);
} catch (Exception e) {
return OrderResult.failure("Order processing failed: " + e.getMessage());
}
}
private String generateOrderId() {
return "ORD" + System.currentTimeMillis();
}
}
// Usage
ComputerFacade computer = new ComputerFacade();
computer.start();
OrderProcessingFacade orderProcessor = new OrderProcessingFacade();
OrderRequest request = new OrderRequest("CUST123", "PROD456", 2, new BigDecimal("99.99"), "123 Main St");
OrderResult result = orderProcessor.processOrder(request);
```
Design patterns provide proven solutions to common programming problems. Understanding when and how to apply these patterns is crucial for writing maintainable, flexible, and robust Java applications. The key is to use patterns judiciously - not every problem requires a pattern, and overuse can lead to unnecessary complexity.