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.