🎯 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
| Pattern | Complexity | Use Cases | Performance Impact |
|---|---|---|---|
| Singleton | Low | Configuration, Logging, Database connections | Minimal |
| Factory | Medium | Object creation abstraction, Plugin systems | Low |
| Builder | Medium | Complex object construction, Immutable objects | Low |
| Decorator | Medium | Feature addition, Middleware systems | Low-Medium |
| Adapter | Low | Legacy system integration, API compatibility | Minimal |
| Observer | Medium | Event systems, MVC architectures | Medium |
| Strategy | Medium | Algorithm selection, Payment processing | Low |
🎯 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:
- Creational Patterns help manage object creation complexity
- Structural Patterns organize object relationships and composition
- Behavioral Patterns define communication and responsibility distribution
Best Practices:
- Don’t overuse patterns - Apply them when they solve real problems
- Understand the trade-offs - Each pattern has advantages and disadvantages
- Consider performance implications - Some patterns add overhead
- Maintain code readability - Patterns should improve, not complicate code
- 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.