Essential Design Patterns in Java: A Comprehensive Guide to Creational, Structural, and Behavioral Patterns

🎯 Introduction

Design patterns are proven solutions to commonly occurring problems in software design. They represent best practices evolved over time and provide a shared vocabulary for developers. This comprehensive guide explores the most essential design patterns in Java, demonstrating practical implementations with real-world examples.

We’ll cover the three main categories of design patterns from the Gang of Four: Creational, Structural, and Behavioral patterns, showing how to implement them effectively in modern Java applications.

🏭 Creational Patterns

Creational patterns deal with object creation mechanisms, trying to create objects in a manner suitable to the situation.

🔧 Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides global access to that instance.

graph TD
    A[Client] --> B[Singleton.getInstance()]
    B --> C{Instance exists?}
    C -->|No| D[Create new instance]
    C -->|Yes| E[Return existing instance]
    D --> F[Store in static field]
    F --> G[Return instance]
    E --> G

    style A fill:#ff6b6b
    style G fill:#4ecdc4
    style C fill:#feca57

🛠️ Thread-Safe Singleton Implementation

1. Enum Singleton (Recommended):

  1public enum DatabaseManager {
  2    INSTANCE;
  3
  4    private Connection connection;
  5    private final Properties config;
  6
  7    DatabaseManager() {
  8        this.config = loadConfiguration();
  9        initializeConnection();
 10    }
 11
 12    public Connection getConnection() {
 13        if (connection == null || !isConnectionValid()) {
 14            synchronized (this) {
 15                if (connection == null || !isConnectionValid()) {
 16                    initializeConnection();
 17                }
 18            }
 19        }
 20        return connection;
 21    }
 22
 23    private void initializeConnection() {
 24        try {
 25            String url = config.getProperty("database.url");
 26            String username = config.getProperty("database.username");
 27            String password = config.getProperty("database.password");
 28
 29            this.connection = DriverManager.getConnection(url, username, password);
 30            System.out.println("Database connection initialized");
 31
 32        } catch (SQLException e) {
 33            throw new RuntimeException("Failed to initialize database connection", e);
 34        }
 35    }
 36
 37    private boolean isConnectionValid() {
 38        try {
 39            return connection != null && !connection.isClosed() && connection.isValid(5);
 40        } catch (SQLException e) {
 41            return false;
 42        }
 43    }
 44
 45    private Properties loadConfiguration() {
 46        Properties props = new Properties();
 47        try (InputStream input = getClass().getClassLoader().getResourceAsStream("database.properties")) {
 48            if (input != null) {
 49                props.load(input);
 50            } else {
 51                // Default configuration
 52                props.setProperty("database.url", "jdbc:h2:mem:testdb");
 53                props.setProperty("database.username", "sa");
 54                props.setProperty("database.password", "");
 55            }
 56        } catch (IOException e) {
 57            throw new RuntimeException("Failed to load database configuration", e);
 58        }
 59        return props;
 60    }
 61
 62    public void executeQuery(String sql) {
 63        try (PreparedStatement stmt = getConnection().prepareStatement(sql)) {
 64            ResultSet rs = stmt.executeQuery();
 65            // Process results
 66            while (rs.next()) {
 67                System.out.println("Query result: " + rs.getString(1));
 68            }
 69        } catch (SQLException e) {
 70            System.err.println("Query execution failed: " + e.getMessage());
 71        }
 72    }
 73
 74    public void closeConnection() {
 75        if (connection != null) {
 76            try {
 77                connection.close();
 78                System.out.println("Database connection closed");
 79            } catch (SQLException e) {
 80                System.err.println("Error closing connection: " + e.getMessage());
 81            }
 82        }
 83    }
 84}
 85
 86// Usage
 87public class DatabaseExample {
 88    public static void main(String[] args) {
 89        // Get singleton instance
 90        DatabaseManager dbManager = DatabaseManager.INSTANCE;
 91
 92        // Use the database manager
 93        dbManager.executeQuery("SELECT 1");
 94
 95        // Same instance everywhere
 96        DatabaseManager anotherReference = DatabaseManager.INSTANCE;
 97        System.out.println("Same instance: " + (dbManager == anotherReference)); // true
 98
 99        // Cleanup
100        dbManager.closeConnection();
101    }
102}

2. Double-Checked Locking Singleton:

 1public class ConfigurationManager {
 2    private static volatile ConfigurationManager instance;
 3    private final Map<String, String> properties;
 4    private final long lastModified;
 5
 6    private ConfigurationManager() {
 7        this.properties = new ConcurrentHashMap<>();
 8        this.lastModified = System.currentTimeMillis();
 9        loadConfiguration();
10    }
11
12    public static ConfigurationManager getInstance() {
13        if (instance == null) {
14            synchronized (ConfigurationManager.class) {
15                if (instance == null) {
16                    instance = new ConfigurationManager();
17                }
18            }
19        }
20        return instance;
21    }
22
23    private void loadConfiguration() {
24        // Simulate loading configuration from file
25        properties.put("app.name", "Design Patterns Demo");
26        properties.put("app.version", "1.0.0");
27        properties.put("app.environment", "production");
28        properties.put("database.pool.size", "20");
29        properties.put("cache.ttl.seconds", "3600");
30
31        System.out.println("Configuration loaded with " + properties.size() + " properties");
32    }
33
34    public String getProperty(String key) {
35        return properties.get(key);
36    }
37
38    public String getProperty(String key, String defaultValue) {
39        return properties.getOrDefault(key, defaultValue);
40    }
41
42    public void setProperty(String key, String value) {
43        properties.put(key, value);
44        System.out.println("Property updated: " + key + " = " + value);
45    }
46
47    public Map<String, String> getAllProperties() {
48        return new HashMap<>(properties);
49    }
50
51    public void reloadConfiguration() {
52        synchronized (this) {
53            properties.clear();
54            loadConfiguration();
55            System.out.println("Configuration reloaded");
56        }
57    }
58}

✅ Pros and Cons

✅ Advantages:

  • Controlled access to sole instance
  • Reduced namespace pollution
  • Lazy initialization possible
  • Thread-safe with proper implementation

❌ Disadvantages:

  • Hidden dependencies (hard to test)
  • Violates Single Responsibility Principle
  • Difficult to subclass
  • Global state can cause issues

🎯 Use Cases:

  • Configuration managers
  • Database connection pools
  • Logging systems
  • Cache managers

🏭 Factory Method Pattern

The Factory Method pattern creates objects without specifying the exact class to create.

graph TD
    A[Client] --> B[NotificationFactory]
    B --> C{Notification Type?}
    C -->|EMAIL| D[EmailNotification]
    C -->|SMS| E[SmsNotification]
    C -->|PUSH| F[PushNotification]

    D --> G[send()]
    E --> G
    F --> G

    style A fill:#ff6b6b
    style B fill:#4ecdc4
    style G fill:#feca57

🛠️ Factory Method Implementation

  1// Notification interface
  2public interface Notification {
  3    void send(String recipient, String message);
  4    String getType();
  5    boolean isDelivered();
  6}
  7
  8// Concrete notification implementations
  9public class EmailNotification implements Notification {
 10    private boolean delivered = false;
 11
 12    @Override
 13    public void send(String recipient, String message) {
 14        System.out.println("Sending EMAIL to " + recipient);
 15        System.out.println("Subject: Notification");
 16        System.out.println("Message: " + message);
 17
 18        // Simulate email sending
 19        try {
 20            Thread.sleep(100);
 21            delivered = Math.random() > 0.1; // 90% success rate
 22
 23            if (delivered) {
 24                System.out.println("Email delivered successfully");
 25            } else {
 26                System.out.println("Email delivery failed");
 27            }
 28        } catch (InterruptedException e) {
 29            Thread.currentThread().interrupt();
 30        }
 31    }
 32
 33    @Override
 34    public String getType() {
 35        return "EMAIL";
 36    }
 37
 38    @Override
 39    public boolean isDelivered() {
 40        return delivered;
 41    }
 42}
 43
 44public class SmsNotification implements Notification {
 45    private boolean delivered = false;
 46
 47    @Override
 48    public void send(String recipient, String message) {
 49        System.out.println("Sending SMS to " + recipient);
 50        System.out.println("Message: " + message);
 51
 52        // Simulate SMS sending
 53        try {
 54            Thread.sleep(50);
 55            delivered = Math.random() > 0.05; // 95% success rate
 56
 57            if (delivered) {
 58                System.out.println("SMS delivered successfully");
 59            } else {
 60                System.out.println("SMS delivery failed");
 61            }
 62        } catch (InterruptedException e) {
 63            Thread.currentThread().interrupt();
 64        }
 65    }
 66
 67    @Override
 68    public String getType() {
 69        return "SMS";
 70    }
 71
 72    @Override
 73    public boolean isDelivered() {
 74        return delivered;
 75    }
 76}
 77
 78public class PushNotification implements Notification {
 79    private boolean delivered = false;
 80
 81    @Override
 82    public void send(String recipient, String message) {
 83        System.out.println("Sending PUSH notification to " + recipient);
 84        System.out.println("Message: " + message);
 85
 86        // Simulate push notification
 87        try {
 88            Thread.sleep(20);
 89            delivered = Math.random() > 0.02; // 98% success rate
 90
 91            if (delivered) {
 92                System.out.println("Push notification delivered successfully");
 93            } else {
 94                System.out.println("Push notification delivery failed");
 95            }
 96        } catch (InterruptedException e) {
 97            Thread.currentThread().interrupt();
 98        }
 99    }
100
101    @Override
102    public String getType() {
103        return "PUSH";
104    }
105
106    @Override
107    public boolean isDelivered() {
108        return delivered;
109    }
110}
111
112// Factory class
113public class NotificationFactory {
114
115    public enum NotificationType {
116        EMAIL, SMS, PUSH
117    }
118
119    // Factory method
120    public static Notification createNotification(NotificationType type) {
121        switch (type) {
122            case EMAIL:
123                return new EmailNotification();
124            case SMS:
125                return new SmsNotification();
126            case PUSH:
127                return new PushNotification();
128            default:
129                throw new IllegalArgumentException("Unknown notification type: " + type);
130        }
131    }
132
133    // Factory method with validation
134    public static Notification createNotification(String type) {
135        try {
136            NotificationType notificationType = NotificationType.valueOf(type.toUpperCase());
137            return createNotification(notificationType);
138        } catch (IllegalArgumentException e) {
139            throw new IllegalArgumentException("Invalid notification type: " + type +
140                ". Supported types: " + Arrays.toString(NotificationType.values()));
141        }
142    }
143
144    // Factory method with multiple notifications
145    public static List<Notification> createNotifications(NotificationType... types) {
146        List<Notification> notifications = new ArrayList<>();
147
148        for (NotificationType type : types) {
149            notifications.add(createNotification(type));
150        }
151
152        return notifications;
153    }
154}
155
156// Notification service using factory
157public class NotificationService {
158    private final List<Notification> sentNotifications = new ArrayList<>();
159
160    public void sendNotification(NotificationType type, String recipient, String message) {
161        Notification notification = NotificationFactory.createNotification(type);
162        notification.send(recipient, message);
163        sentNotifications.add(notification);
164    }
165
166    public void sendMultiChannelNotification(String recipient, String message,
167                                           NotificationType... channels) {
168        List<Notification> notifications = NotificationFactory.createNotifications(channels);
169
170        for (Notification notification : notifications) {
171            notification.send(recipient, message);
172            sentNotifications.add(notification);
173        }
174    }
175
176    public void printDeliveryReport() {
177        System.out.println("\n=== Delivery Report ===");
178        Map<String, Long> deliveryStats = sentNotifications.stream()
179            .collect(Collectors.groupingBy(
180                Notification::getType,
181                Collectors.counting()
182            ));
183
184        Map<String, Long> successStats = sentNotifications.stream()
185            .filter(Notification::isDelivered)
186            .collect(Collectors.groupingBy(
187                Notification::getType,
188                Collectors.counting()
189            ));
190
191        deliveryStats.forEach((type, total) -> {
192            long successful = successStats.getOrDefault(type, 0L);
193            double successRate = (double) successful / total * 100;
194            System.out.printf("%s: %d sent, %d delivered (%.1f%% success rate)%n",
195                type, total, successful, successRate);
196        });
197    }
198}
199
200// Usage example
201public class FactoryPatternExample {
202    public static void main(String[] args) {
203        NotificationService service = new NotificationService();
204
205        // Send individual notifications
206        service.sendNotification(NotificationType.EMAIL, "user@example.com", "Welcome to our service!");
207        service.sendNotification(NotificationType.SMS, "+1234567890", "Your verification code is 12345");
208        service.sendNotification(NotificationType.PUSH, "user_device_id", "You have a new message");
209
210        // Send multi-channel notification
211        service.sendMultiChannelNotification(
212            "premium_user@example.com",
213            "Your premium subscription expires soon",
214            NotificationType.EMAIL,
215            NotificationType.PUSH
216        );
217
218        // Print delivery statistics
219        service.printDeliveryReport();
220    }
221}

🔨 Builder Pattern

The Builder pattern constructs complex objects step by step, allowing different representations using the same construction process.

graph TD
    A[Client] --> B[UserBuilder]
    B --> C[setName()]
    C --> D[setEmail()]
    D --> E[setAge()]
    E --> F[addRole()]
    F --> G[build()]
    G --> H[User Object]

    style A fill:#ff6b6b
    style B fill:#4ecdc4
    style H fill:#feca57

🛠️ Builder Pattern Implementation

  1// Complex User class
  2public class User {
  3    // Required parameters
  4    private final String username;
  5    private final String email;
  6
  7    // Optional parameters
  8    private final String firstName;
  9    private final String lastName;
 10    private final int age;
 11    private final String phone;
 12    private final String address;
 13    private final List<String> roles;
 14    private final boolean isActive;
 15    private final LocalDateTime createdAt;
 16    private final Map<String, String> preferences;
 17
 18    // Private constructor - only Builder can create instances
 19    private User(UserBuilder builder) {
 20        this.username = builder.username;
 21        this.email = builder.email;
 22        this.firstName = builder.firstName;
 23        this.lastName = builder.lastName;
 24        this.age = builder.age;
 25        this.phone = builder.phone;
 26        this.address = builder.address;
 27        this.roles = Collections.unmodifiableList(new ArrayList<>(builder.roles));
 28        this.isActive = builder.isActive;
 29        this.createdAt = builder.createdAt;
 30        this.preferences = Collections.unmodifiableMap(new HashMap<>(builder.preferences));
 31    }
 32
 33    // Getters
 34    public String getUsername() { return username; }
 35    public String getEmail() { return email; }
 36    public String getFirstName() { return firstName; }
 37    public String getLastName() { return lastName; }
 38    public int getAge() { return age; }
 39    public String getPhone() { return phone; }
 40    public String getAddress() { return address; }
 41    public List<String> getRoles() { return roles; }
 42    public boolean isActive() { return isActive; }
 43    public LocalDateTime getCreatedAt() { return createdAt; }
 44    public Map<String, String> getPreferences() { return preferences; }
 45
 46    public String getFullName() {
 47        if (firstName != null && lastName != null) {
 48            return firstName + " " + lastName;
 49        } else if (firstName != null) {
 50            return firstName;
 51        } else if (lastName != null) {
 52            return lastName;
 53        } else {
 54            return username;
 55        }
 56    }
 57
 58    public boolean hasRole(String role) {
 59        return roles.contains(role);
 60    }
 61
 62    @Override
 63    public String toString() {
 64        return String.format("User{username='%s', email='%s', fullName='%s', roles=%s, active=%s}",
 65            username, email, getFullName(), roles, isActive);
 66    }
 67
 68    // Static method to create builder
 69    public static UserBuilder builder(String username, String email) {
 70        return new UserBuilder(username, email);
 71    }
 72
 73    // Builder class
 74    public static class UserBuilder {
 75        // Required parameters
 76        private final String username;
 77        private final String email;
 78
 79        // Optional parameters with default values
 80        private String firstName;
 81        private String lastName;
 82        private int age = 0;
 83        private String phone;
 84        private String address;
 85        private List<String> roles = new ArrayList<>();
 86        private boolean isActive = true;
 87        private LocalDateTime createdAt = LocalDateTime.now();
 88        private Map<String, String> preferences = new HashMap<>();
 89
 90        // Constructor with required parameters
 91        public UserBuilder(String username, String email) {
 92            if (username == null || username.trim().isEmpty()) {
 93                throw new IllegalArgumentException("Username cannot be null or empty");
 94            }
 95            if (email == null || !isValidEmail(email)) {
 96                throw new IllegalArgumentException("Invalid email address");
 97            }
 98
 99            this.username = username.trim();
100            this.email = email.toLowerCase().trim();
101        }
102
103        public UserBuilder firstName(String firstName) {
104            this.firstName = firstName;
105            return this;
106        }
107
108        public UserBuilder lastName(String lastName) {
109            this.lastName = lastName;
110            return this;
111        }
112
113        public UserBuilder age(int age) {
114            if (age < 0 || age > 120) {
115                throw new IllegalArgumentException("Age must be between 0 and 120");
116            }
117            this.age = age;
118            return this;
119        }
120
121        public UserBuilder phone(String phone) {
122            this.phone = phone;
123            return this;
124        }
125
126        public UserBuilder address(String address) {
127            this.address = address;
128            return this;
129        }
130
131        public UserBuilder addRole(String role) {
132            if (role != null && !role.trim().isEmpty()) {
133                this.roles.add(role.trim().toUpperCase());
134            }
135            return this;
136        }
137
138        public UserBuilder roles(String... roles) {
139            for (String role : roles) {
140                addRole(role);
141            }
142            return this;
143        }
144
145        public UserBuilder roles(List<String> roles) {
146            for (String role : roles) {
147                addRole(role);
148            }
149            return this;
150        }
151
152        public UserBuilder active(boolean isActive) {
153            this.isActive = isActive;
154            return this;
155        }
156
157        public UserBuilder createdAt(LocalDateTime createdAt) {
158            this.createdAt = createdAt != null ? createdAt : LocalDateTime.now();
159            return this;
160        }
161
162        public UserBuilder preference(String key, String value) {
163            if (key != null && value != null) {
164                this.preferences.put(key, value);
165            }
166            return this;
167        }
168
169        public UserBuilder preferences(Map<String, String> preferences) {
170            if (preferences != null) {
171                this.preferences.putAll(preferences);
172            }
173            return this;
174        }
175
176        // Build method with validation
177        public User build() {
178            validateBuild();
179            return new User(this);
180        }
181
182        private void validateBuild() {
183            // Additional validation before building
184            if (age > 0 && age < 13 && roles.contains("ADMIN")) {
185                throw new IllegalStateException("Users under 13 cannot have admin role");
186            }
187
188            // Ensure at least one role is assigned
189            if (roles.isEmpty()) {
190                roles.add("USER");
191            }
192        }
193
194        private boolean isValidEmail(String email) {
195            return email != null && email.matches("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$");
196        }
197    }
198}
199
200// Usage examples
201public class BuilderPatternExample {
202    public static void main(String[] args) {
203        // Simple user
204        User basicUser = User.builder("johndoe", "john@example.com")
205            .firstName("John")
206            .lastName("Doe")
207            .build();
208
209        System.out.println("Basic user: " + basicUser);
210
211        // Complex user with all options
212        User adminUser = User.builder("admin", "admin@company.com")
213            .firstName("System")
214            .lastName("Administrator")
215            .age(35)
216            .phone("+1-555-0123")
217            .address("123 Admin Street, Server City, Cloud State 12345")
218            .roles("ADMIN", "USER", "MODERATOR")
219            .preference("theme", "dark")
220            .preference("notifications", "enabled")
221            .preference("language", "en-US")
222            .build();
223
224        System.out.println("Admin user: " + adminUser);
225        System.out.println("Admin roles: " + adminUser.getRoles());
226        System.out.println("Admin preferences: " + adminUser.getPreferences());
227
228        // User with custom creation time
229        User historicalUser = User.builder("historical", "historical@example.com")
230            .firstName("Historical")
231            .lastName("User")
232            .createdAt(LocalDateTime.of(2020, 1, 1, 0, 0))
233            .active(false)
234            .roles(Arrays.asList("LEGACY_USER", "ARCHIVED"))
235            .build();
236
237        System.out.println("Historical user: " + historicalUser);
238        System.out.println("Created at: " + historicalUser.getCreatedAt());
239
240        // Demonstrate method chaining flexibility
241        User flexibleUser = User.builder("flexible", "flexible@example.com")
242            .addRole("USER")
243            .age(28)
244            .addRole("PREMIUM")
245            .firstName("Flexible")
246            .preference("tier", "premium")
247            .addRole("BETA_TESTER")
248            .build();
249
250        System.out.println("Flexible user: " + flexibleUser);
251
252        // Building users in a factory-like method
253        createSampleUsers();
254    }
255
256    private static void createSampleUsers() {
257        System.out.println("\n=== Sample Users ===");
258
259        List<User> users = Arrays.asList(
260            User.builder("user1", "user1@example.com")
261                .firstName("Alice")
262                .lastName("Smith")
263                .age(29)
264                .roles("USER", "PREMIUM")
265                .build(),
266
267            User.builder("user2", "user2@example.com")
268                .firstName("Bob")
269                .lastName("Johnson")
270                .age(34)
271                .addRole("USER")
272                .preference("newsletter", "weekly")
273                .build(),
274
275            User.builder("user3", "user3@example.com")
276                .firstName("Charlie")
277                .age(42)
278                .roles("ADMIN", "USER")
279                .phone("+1-555-9999")
280                .build()
281        );
282
283        users.forEach(System.out::println);
284
285        // Demonstrate role checking
286        System.out.println("\nAdmin users:");
287        users.stream()
288            .filter(user -> user.hasRole("ADMIN"))
289            .forEach(user -> System.out.println("  " + user.getFullName()));
290    }
291}

✅ Pros and Cons

✅ Advantages:

  • Flexible object construction
  • Immutable objects possible
  • Clear, readable code
  • Step-by-step construction

❌ Disadvantages:

  • More verbose code
  • Additional classes needed
  • Memory overhead
  • Complex for simple objects

🎯 Use Cases:

  • Complex configuration objects
  • Immutable data transfer objects
  • SQL query builders
  • Test data builders

🏗️ Structural Patterns

Structural patterns deal with object composition, creating relationships between objects to form larger structures.

🎭 Decorator Pattern

The Decorator pattern attaches additional responsibilities to objects dynamically, providing a flexible alternative to subclassing.

graph TD
    A[Client] --> B[Coffee Interface]
    B --> C[SimpleCoffee]
    B --> D[CoffeeDecorator]

    D --> E[MilkDecorator]
    D --> F[SugarDecorator]
    D --> G[WhipDecorator]

    E --> H[wraps Coffee]
    F --> H
    G --> H

    style A fill:#ff6b6b
    style B fill:#4ecdc4
    style H fill:#feca57

🛠️ Decorator Pattern Implementation

  1// Base coffee interface
  2public interface Coffee {
  3    String getDescription();
  4    double getCost();
  5    List<String> getIngredients();
  6}
  7
  8// Base coffee implementation
  9public class SimpleCoffee implements Coffee {
 10    @Override
 11    public String getDescription() {
 12        return "Simple Coffee";
 13    }
 14
 15    @Override
 16    public double getCost() {
 17        return 2.00;
 18    }
 19
 20    @Override
 21    public List<String> getIngredients() {
 22        return Arrays.asList("Coffee Beans", "Water");
 23    }
 24}
 25
 26// Base decorator
 27public abstract class CoffeeDecorator implements Coffee {
 28    protected final Coffee coffee;
 29
 30    public CoffeeDecorator(Coffee coffee) {
 31        this.coffee = coffee;
 32    }
 33
 34    @Override
 35    public String getDescription() {
 36        return coffee.getDescription();
 37    }
 38
 39    @Override
 40    public double getCost() {
 41        return coffee.getCost();
 42    }
 43
 44    @Override
 45    public List<String> getIngredients() {
 46        return new ArrayList<>(coffee.getIngredients());
 47    }
 48}
 49
 50// Concrete decorators
 51public class MilkDecorator extends CoffeeDecorator {
 52    public MilkDecorator(Coffee coffee) {
 53        super(coffee);
 54    }
 55
 56    @Override
 57    public String getDescription() {
 58        return coffee.getDescription() + ", with Milk";
 59    }
 60
 61    @Override
 62    public double getCost() {
 63        return coffee.getCost() + 0.50;
 64    }
 65
 66    @Override
 67    public List<String> getIngredients() {
 68        List<String> ingredients = super.getIngredients();
 69        ingredients.add("Milk");
 70        return ingredients;
 71    }
 72}
 73
 74public class SugarDecorator extends CoffeeDecorator {
 75    private final int cubes;
 76
 77    public SugarDecorator(Coffee coffee, int cubes) {
 78        super(coffee);
 79        this.cubes = cubes;
 80    }
 81
 82    public SugarDecorator(Coffee coffee) {
 83        this(coffee, 1);
 84    }
 85
 86    @Override
 87    public String getDescription() {
 88        return coffee.getDescription() + ", with " + cubes + " sugar cube(s)";
 89    }
 90
 91    @Override
 92    public double getCost() {
 93        return coffee.getCost() + (0.25 * cubes);
 94    }
 95
 96    @Override
 97    public List<String> getIngredients() {
 98        List<String> ingredients = super.getIngredients();
 99        ingredients.add("Sugar (" + cubes + " cube" + (cubes > 1 ? "s" : "") + ")");
100        return ingredients;
101    }
102}
103
104public class WhipDecorator extends CoffeeDecorator {
105    public WhipDecorator(Coffee coffee) {
106        super(coffee);
107    }
108
109    @Override
110    public String getDescription() {
111        return coffee.getDescription() + ", with Whipped Cream";
112    }
113
114    @Override
115    public double getCost() {
116        return coffee.getCost() + 0.75;
117    }
118
119    @Override
120    public List<String> getIngredients() {
121        List<String> ingredients = super.getIngredients();
122        ingredients.add("Whipped Cream");
123        return ingredients;
124    }
125}
126
127public class VanillaDecorator extends CoffeeDecorator {
128    public VanillaDecorator(Coffee coffee) {
129        super(coffee);
130    }
131
132    @Override
133    public String getDescription() {
134        return coffee.getDescription() + ", with Vanilla Syrup";
135    }
136
137    @Override
138    public double getCost() {
139        return coffee.getCost() + 0.60;
140    }
141
142    @Override
143    public List<String> getIngredients() {
144        List<String> ingredients = super.getIngredients();
145        ingredients.add("Vanilla Syrup");
146        return ingredients;
147    }
148}
149
150// Coffee shop service
151public class CoffeeShop {
152
153    public void printOrder(Coffee coffee) {
154        System.out.println("Order Details:");
155        System.out.println("Description: " + coffee.getDescription());
156        System.out.println("Cost: $" + String.format("%.2f", coffee.getCost()));
157        System.out.println("Ingredients: " + String.join(", ", coffee.getIngredients()));
158        System.out.println("-".repeat(50));
159    }
160
161    // Pre-configured coffee combinations
162    public Coffee createLatte() {
163        return new MilkDecorator(
164            new VanillaDecorator(
165                new SimpleCoffee()
166            )
167        );
168    }
169
170    public Coffee createMochaWithWhip() {
171        return new WhipDecorator(
172            new VanillaDecorator(
173                new MilkDecorator(
174                    new SimpleCoffee()
175                )
176            )
177        );
178    }
179
180    public Coffee createSweetCoffee() {
181        return new SugarDecorator(
182            new MilkDecorator(
183                new SimpleCoffee()
184            ),
185            3 // 3 sugar cubes
186        );
187    }
188}
189
190// Usage example
191public class DecoratorPatternExample {
192    public static void main(String[] args) {
193        CoffeeShop shop = new CoffeeShop();
194
195        // Basic coffee
196        Coffee simpleCoffee = new SimpleCoffee();
197        shop.printOrder(simpleCoffee);
198
199        // Coffee with milk
200        Coffee coffeeWithMilk = new MilkDecorator(simpleCoffee);
201        shop.printOrder(coffeeWithMilk);
202
203        // Complex decorated coffee
204        Coffee fancyCoffee = new WhipDecorator(
205            new VanillaDecorator(
206                new SugarDecorator(
207                    new MilkDecorator(
208                        new SimpleCoffee()
209                    ), 2
210                )
211            )
212        );
213        shop.printOrder(fancyCoffee);
214
215        // Pre-configured coffees
216        System.out.println("=== Pre-configured Coffees ===");
217
218        Coffee latte = shop.createLatte();
219        shop.printOrder(latte);
220
221        Coffee mocha = shop.createMochaWithWhip();
222        shop.printOrder(mocha);
223
224        Coffee sweetCoffee = shop.createSweetCoffee();
225        shop.printOrder(sweetCoffee);
226
227        // Demonstrate decorator flexibility
228        demonstrateFlexibility();
229    }
230
231    private static void demonstrateFlexibility() {
232        System.out.println("=== Decorator Flexibility ===");
233
234        // Start with simple coffee
235        Coffee coffee = new SimpleCoffee();
236        System.out.println("1. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
237
238        // Add decorators one by one
239        coffee = new MilkDecorator(coffee);
240        System.out.println("2. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
241
242        coffee = new SugarDecorator(coffee, 2);
243        System.out.println("3. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
244
245        coffee = new VanillaDecorator(coffee);
246        System.out.println("4. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
247
248        coffee = new WhipDecorator(coffee);
249        System.out.println("5. " + coffee.getDescription() + " - $" + String.format("%.2f", coffee.getCost()));
250
251        System.out.println("Final ingredients: " + String.join(", ", coffee.getIngredients()));
252    }
253}

🔌 Adapter Pattern

The Adapter pattern allows incompatible interfaces to work together by acting as a bridge between them.

🛠️ Adapter Pattern Implementation

  1// Target interface (what we want)
  2public interface MediaPlayer {
  3    void play(String audioType, String fileName);
  4}
  5
  6// Adaptee interfaces (what we have)
  7public interface AdvancedMediaPlayer {
  8    void playVlc(String fileName);
  9    void playMp4(String fileName);
 10    void playMov(String fileName);
 11}
 12
 13// Concrete adaptee implementations
 14public class VlcPlayer implements AdvancedMediaPlayer {
 15    @Override
 16    public void playVlc(String fileName) {
 17        System.out.println("Playing vlc file: " + fileName);
 18    }
 19
 20    @Override
 21    public void playMp4(String fileName) {
 22        // Do nothing - not supported
 23    }
 24
 25    @Override
 26    public void playMov(String fileName) {
 27        // Do nothing - not supported
 28    }
 29}
 30
 31public class Mp4Player implements AdvancedMediaPlayer {
 32    @Override
 33    public void playVlc(String fileName) {
 34        // Do nothing - not supported
 35    }
 36
 37    @Override
 38    public void playMp4(String fileName) {
 39        System.out.println("Playing mp4 file: " + fileName);
 40    }
 41
 42    @Override
 43    public void playMov(String fileName) {
 44        // Do nothing - not supported
 45    }
 46}
 47
 48// Adapter class
 49public class MediaAdapter implements MediaPlayer {
 50    private final AdvancedMediaPlayer advancedPlayer;
 51
 52    public MediaAdapter(String audioType) {
 53        switch (audioType.toLowerCase()) {
 54            case "vlc":
 55                advancedPlayer = new VlcPlayer();
 56                break;
 57            case "mp4":
 58                advancedPlayer = new Mp4Player();
 59                break;
 60            default:
 61                throw new IllegalArgumentException("Unsupported audio type: " + audioType);
 62        }
 63    }
 64
 65    @Override
 66    public void play(String audioType, String fileName) {
 67        switch (audioType.toLowerCase()) {
 68            case "vlc":
 69                advancedPlayer.playVlc(fileName);
 70                break;
 71            case "mp4":
 72                advancedPlayer.playMp4(fileName);
 73                break;
 74            default:
 75                System.out.println("Invalid media. " + audioType + " format not supported");
 76        }
 77    }
 78}
 79
 80// Context class that uses the adapter
 81public class AudioPlayer implements MediaPlayer {
 82    private MediaAdapter mediaAdapter;
 83
 84    @Override
 85    public void play(String audioType, String fileName) {
 86        // Built-in support for mp3
 87        if (audioType.equalsIgnoreCase("mp3")) {
 88            System.out.println("Playing mp3 file: " + fileName);
 89        }
 90        // Use adapter for other formats
 91        else if (audioType.equalsIgnoreCase("vlc") || audioType.equalsIgnoreCase("mp4")) {
 92            try {
 93                mediaAdapter = new MediaAdapter(audioType);
 94                mediaAdapter.play(audioType, fileName);
 95            } catch (IllegalArgumentException e) {
 96                System.out.println("Error: " + e.getMessage());
 97            }
 98        }
 99        else {
100            System.out.println("Invalid media. " + audioType + " format not supported");
101        }
102    }
103}

🎯 Behavioral Patterns

Behavioral patterns focus on communication between objects and the assignment of responsibilities between objects.

👁️ Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified.

🛠️ Observer Pattern Implementation

  1// Observer interface
  2public interface Observer {
  3    void update(String message);
  4    String getObserverId();
  5}
  6
  7// Subject interface
  8public interface Subject {
  9    void attach(Observer observer);
 10    void detach(Observer observer);
 11    void notifyObservers(String message);
 12}
 13
 14// Concrete subject
 15public class NewsAgency implements Subject {
 16    private final List<Observer> observers = new ArrayList<>();
 17    private String latestNews;
 18
 19    @Override
 20    public void attach(Observer observer) {
 21        observers.add(observer);
 22        System.out.println("Observer " + observer.getObserverId() + " attached");
 23    }
 24
 25    @Override
 26    public void detach(Observer observer) {
 27        observers.remove(observer);
 28        System.out.println("Observer " + observer.getObserverId() + " detached");
 29    }
 30
 31    @Override
 32    public void notifyObservers(String message) {
 33        System.out.println("Notifying " + observers.size() + " observers about: " + message);
 34        for (Observer observer : observers) {
 35            observer.update(message);
 36        }
 37    }
 38
 39    public void publishNews(String news) {
 40        this.latestNews = news;
 41        notifyObservers(news);
 42    }
 43
 44    public String getLatestNews() {
 45        return latestNews;
 46    }
 47}
 48
 49// Concrete observers
 50public class NewsChannel implements Observer {
 51    private final String channelId;
 52    private String lastNews;
 53
 54    public NewsChannel(String channelId) {
 55        this.channelId = channelId;
 56    }
 57
 58    @Override
 59    public void update(String message) {
 60        this.lastNews = message;
 61        System.out.println("[" + channelId + "] Broadcasting: " + message);
 62    }
 63
 64    @Override
 65    public String getObserverId() {
 66        return channelId;
 67    }
 68
 69    public String getLastNews() {
 70        return lastNews;
 71    }
 72}
 73
 74public class MobileApp implements Observer {
 75    private final String appId;
 76    private final List<String> notifications = new ArrayList<>();
 77
 78    public MobileApp(String appId) {
 79        this.appId = appId;
 80    }
 81
 82    @Override
 83    public void update(String message) {
 84        notifications.add(message);
 85        System.out.println("[" + appId + "] Push notification: " + message);
 86    }
 87
 88    @Override
 89    public String getObserverId() {
 90        return appId;
 91    }
 92
 93    public List<String> getNotifications() {
 94        return new ArrayList<>(notifications);
 95    }
 96}
 97
 98// Usage example
 99public class ObserverPatternExample {
100    public static void main(String[] args) {
101        // Create subject
102        NewsAgency newsAgency = new NewsAgency();
103
104        // Create observers
105        NewsChannel cnn = new NewsChannel("CNN");
106        NewsChannel bbc = new NewsChannel("BBC");
107        MobileApp newsApp = new MobileApp("NewsApp");
108        MobileApp alertApp = new MobileApp("AlertApp");
109
110        // Attach observers
111        newsAgency.attach(cnn);
112        newsAgency.attach(bbc);
113        newsAgency.attach(newsApp);
114
115        // Publish news
116        newsAgency.publishNews("Breaking: New design patterns guide published!");
117        System.out.println();
118
119        // Add another observer
120        newsAgency.attach(alertApp);
121        newsAgency.publishNews("Technology: Java 21 features announced");
122        System.out.println();
123
124        // Remove an observer
125        newsAgency.detach(bbc);
126        newsAgency.publishNews("Sports: World Cup results are in!");
127        System.out.println();
128
129        // Display observer states
130        System.out.println("=== Observer States ===");
131        System.out.println("CNN last news: " + cnn.getLastNews());
132        System.out.println("BBC last news: " + bbc.getLastNews());
133        System.out.println("NewsApp notifications: " + newsApp.getNotifications().size());
134        System.out.println("AlertApp notifications: " + alertApp.getNotifications().size());
135    }
136}

🎛️ Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.

🛠️ Strategy Pattern Implementation

  1// Strategy interface
  2public interface PaymentStrategy {
  3    boolean pay(double amount);
  4    String getPaymentMethod();
  5}
  6
  7// Concrete strategies
  8public class CreditCardPayment implements PaymentStrategy {
  9    private final String cardNumber;
 10    private final String expiryDate;
 11    private final String cvv;
 12
 13    public CreditCardPayment(String cardNumber, String expiryDate, String cvv) {
 14        this.cardNumber = cardNumber;
 15        this.expiryDate = expiryDate;
 16        this.cvv = cvv;
 17    }
 18
 19    @Override
 20    public boolean pay(double amount) {
 21        System.out.println("Processing credit card payment of $" + amount);
 22        System.out.println("Card: ****-****-****-" + cardNumber.substring(cardNumber.length() - 4));
 23
 24        // Simulate payment processing
 25        try {
 26            Thread.sleep(1000);
 27            boolean success = Math.random() > 0.1; // 90% success rate
 28
 29            if (success) {
 30                System.out.println("Credit card payment successful!");
 31                return true;
 32            } else {
 33                System.out.println("Credit card payment declined!");
 34                return false;
 35            }
 36        } catch (InterruptedException e) {
 37            Thread.currentThread().interrupt();
 38            return false;
 39        }
 40    }
 41
 42    @Override
 43    public String getPaymentMethod() {
 44        return "Credit Card";
 45    }
 46}
 47
 48public class PayPalPayment implements PaymentStrategy {
 49    private final String email;
 50    private final String password;
 51
 52    public PayPalPayment(String email, String password) {
 53        this.email = email;
 54        this.password = password;
 55    }
 56
 57    @Override
 58    public boolean pay(double amount) {
 59        System.out.println("Processing PayPal payment of $" + amount);
 60        System.out.println("PayPal account: " + email);
 61
 62        // Simulate payment processing
 63        try {
 64            Thread.sleep(800);
 65            boolean success = Math.random() > 0.05; // 95% success rate
 66
 67            if (success) {
 68                System.out.println("PayPal payment successful!");
 69                return true;
 70            } else {
 71                System.out.println("PayPal payment failed!");
 72                return false;
 73            }
 74        } catch (InterruptedException e) {
 75            Thread.currentThread().interrupt();
 76            return false;
 77        }
 78    }
 79
 80    @Override
 81    public String getPaymentMethod() {
 82        return "PayPal";
 83    }
 84}
 85
 86public class BankTransferPayment implements PaymentStrategy {
 87    private final String accountNumber;
 88    private final String routingNumber;
 89
 90    public BankTransferPayment(String accountNumber, String routingNumber) {
 91        this.accountNumber = accountNumber;
 92        this.routingNumber = routingNumber;
 93    }
 94
 95    @Override
 96    public boolean pay(double amount) {
 97        System.out.println("Processing bank transfer payment of $" + amount);
 98        System.out.println("Account: ****" + accountNumber.substring(accountNumber.length() - 4));
 99
100        // Simulate payment processing
101        try {
102            Thread.sleep(1500); // Bank transfers take longer
103            boolean success = Math.random() > 0.02; // 98% success rate
104
105            if (success) {
106                System.out.println("Bank transfer payment successful!");
107                return true;
108            } else {
109                System.out.println("Bank transfer payment failed!");
110                return false;
111            }
112        } catch (InterruptedException e) {
113            Thread.currentThread().interrupt();
114            return false;
115        }
116    }
117
118    @Override
119    public String getPaymentMethod() {
120        return "Bank Transfer";
121    }
122}
123
124// Context class
125public class ShoppingCart {
126    private final List<Item> items = new ArrayList<>();
127    private PaymentStrategy paymentStrategy;
128
129    public void addItem(Item item) {
130        items.add(item);
131        System.out.println("Added to cart: " + item.getName() + " - $" + item.getPrice());
132    }
133
134    public void removeItem(Item item) {
135        items.remove(item);
136        System.out.println("Removed from cart: " + item.getName());
137    }
138
139    public double getTotalAmount() {
140        return items.stream().mapToDouble(Item::getPrice).sum();
141    }
142
143    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
144        this.paymentStrategy = paymentStrategy;
145        System.out.println("Payment method set to: " + paymentStrategy.getPaymentMethod());
146    }
147
148    public boolean checkout() {
149        if (items.isEmpty()) {
150            System.out.println("Cart is empty!");
151            return false;
152        }
153
154        if (paymentStrategy == null) {
155            System.out.println("Please select a payment method!");
156            return false;
157        }
158
159        double total = getTotalAmount();
160        System.out.println("\n=== Checkout ===");
161        System.out.println("Items in cart: " + items.size());
162        items.forEach(item -> System.out.println("  " + item.getName() + " - $" + item.getPrice()));
163        System.out.println("Total amount: $" + total);
164        System.out.println();
165
166        boolean paymentSuccess = paymentStrategy.pay(total);
167
168        if (paymentSuccess) {
169            System.out.println("Order completed successfully!");
170            items.clear();
171        } else {
172            System.out.println("Order failed. Please try again.");
173        }
174
175        return paymentSuccess;
176    }
177
178    public void displayCart() {
179        System.out.println("\n=== Shopping Cart ===");
180        if (items.isEmpty()) {
181            System.out.println("Cart is empty");
182        } else {
183            items.forEach(item -> System.out.println(item.getName() + " - $" + item.getPrice()));
184            System.out.println("Total: $" + getTotalAmount());
185        }
186        System.out.println();
187    }
188
189    // Item class
190    public static class Item {
191        private final String name;
192        private final double price;
193
194        public Item(String name, double price) {
195            this.name = name;
196            this.price = price;
197        }
198
199        public String getName() { return name; }
200        public double getPrice() { return price; }
201
202        @Override
203        public boolean equals(Object o) {
204            if (this == o) return true;
205            if (o == null || getClass() != o.getClass()) return false;
206            Item item = (Item) o;
207            return Double.compare(item.price, price) == 0 && Objects.equals(name, item.name);
208        }
209
210        @Override
211        public int hashCode() {
212            return Objects.hash(name, price);
213        }
214    }
215}
216
217// Usage example
218public class StrategyPatternExample {
219    public static void main(String[] args) {
220        ShoppingCart cart = new ShoppingCart();
221
222        // Add items to cart
223        cart.addItem(new ShoppingCart.Item("Laptop", 999.99));
224        cart.addItem(new ShoppingCart.Item("Mouse", 29.99));
225        cart.addItem(new ShoppingCart.Item("Keyboard", 79.99));
226
227        cart.displayCart();
228
229        // Try checkout without payment method
230        cart.checkout();
231
232        // Set payment strategy and checkout
233        System.out.println("\n--- Trying Credit Card Payment ---");
234        cart.setPaymentStrategy(new CreditCardPayment("1234567890123456", "12/25", "123"));
235        cart.checkout();
236
237        // Add more items and try different payment method
238        cart.addItem(new ShoppingCart.Item("Monitor", 299.99));
239        cart.addItem(new ShoppingCart.Item("Speakers", 149.99));
240
241        System.out.println("\n--- Trying PayPal Payment ---");
242        cart.setPaymentStrategy(new PayPalPayment("user@example.com", "password"));
243        cart.checkout();
244
245        // Add items and try bank transfer
246        cart.addItem(new ShoppingCart.Item("Webcam", 89.99));
247
248        System.out.println("\n--- Trying Bank Transfer Payment ---");
249        cart.setPaymentStrategy(new BankTransferPayment("9876543210", "123456789"));
250        cart.checkout();
251
252        cart.displayCart();
253    }
254}

📊 Pattern Comparison and Selection Guide

🎯 When to Use Each Pattern

graph TD
    A[Design Pattern Selection] --> B{Problem Type?}

    B -->|Object Creation| C[Creational Patterns]
    B -->|Object Composition| D[Structural Patterns]
    B -->|Object Interaction| E[Behavioral Patterns]

    C --> F[Singleton - Single instance needed]
    C --> G[Factory - Multiple object types]
    C --> H[Builder - Complex object construction]

    D --> I[Decorator - Add behavior dynamically]
    D --> J[Adapter - Interface compatibility]

    E --> K[Observer - State change notifications]
    E --> L[Strategy - Algorithm selection]

    style C fill:#ff6b6b
    style D fill:#4ecdc4
    style E fill:#feca57
PatternComplexityUse CasesPerformance Impact
SingletonLowConfiguration, Logging, Database connectionsMinimal
FactoryMediumObject creation abstraction, Plugin systemsLow
BuilderMediumComplex object construction, Immutable objectsLow
DecoratorMediumFeature addition, Middleware systemsLow-Medium
AdapterLowLegacy system integration, API compatibilityMinimal
ObserverMediumEvent systems, MVC architecturesMedium
StrategyMediumAlgorithm selection, Payment processingLow

🎯 Conclusion

Design patterns are essential tools in a Java developer’s toolkit, providing proven solutions to common design problems. Each pattern serves specific purposes and offers unique advantages:

Key Takeaways:

  1. Creational Patterns help manage object creation complexity
  2. Structural Patterns organize object relationships and composition
  3. Behavioral Patterns define communication and responsibility distribution

Best Practices:

  1. Don’t overuse patterns - Apply them when they solve real problems
  2. Understand the trade-offs - Each pattern has advantages and disadvantages
  3. Consider performance implications - Some patterns add overhead
  4. Maintain code readability - Patterns should improve, not complicate code
  5. Test thoroughly - Complex patterns require comprehensive testing

Master these fundamental patterns and you’ll be well-equipped to design robust, maintainable, and scalable Java applications.