The Bridge Pattern is a structural design pattern that separates abstraction from implementation, allowing them to vary independently. It's particularly useful for avoiding permanent binding between an abstraction and its implementation.
1. Basic Bridge Pattern Implementation
Core Structure
// Implementation interface
interface DrawingAPI {
void drawCircle(double x, double y, double radius);
void drawRectangle(double x, double y, double width, double height);
}
// Concrete implementations
class VectorDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("Drawing vector circle at (%.1f, %.1f) with radius %.1f\n", x, y, radius);
}
@Override
public void drawRectangle(double x, double y, double width, double height) {
System.out.printf("Drawing vector rectangle at (%.1f, %.1f) with size %.1fx%.1f\n",
x, y, width, height);
}
}
class RasterDrawingAPI implements DrawingAPI {
@Override
public void drawCircle(double x, double y, double radius) {
System.out.printf("Drawing raster circle at (%.1f, %.1f) with radius %.1f\n", x, y, radius);
}
@Override
public void drawRectangle(double x, double y, double width, double height) {
System.out.printf("Drawing raster rectangle at (%.1f, %.1f) with size %.1fx%.1f\n",
x, y, width, height);
}
}
// Abstraction
abstract class Shape {
protected DrawingAPI drawingAPI;
protected Shape(DrawingAPI drawingAPI) {
this.drawingAPI = drawingAPI;
}
public abstract void draw();
public abstract void resize(double factor);
}
// Refined abstractions
class Circle extends Shape {
private double x, y, radius;
public Circle(double x, double y, double radius, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.radius = radius;
}
@Override
public void draw() {
drawingAPI.drawCircle(x, y, radius);
}
@Override
public void resize(double factor) {
radius *= factor;
System.out.printf("Circle resized to radius %.1f\n", radius);
}
}
class Rectangle extends Shape {
private double x, y, width, height;
public Rectangle(double x, double y, double width, double height, DrawingAPI drawingAPI) {
super(drawingAPI);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
@Override
public void draw() {
drawingAPI.drawRectangle(x, y, width, height);
}
@Override
public void resize(double factor) {
width *= factor;
height *= factor;
System.out.printf("Rectangle resized to %.1fx%.1f\n", width, height);
}
}
// Client code
public class BridgePatternDemo {
public static void main(String[] args) {
DrawingAPI vectorAPI = new VectorDrawingAPI();
DrawingAPI rasterAPI = new RasterDrawingAPI();
Shape circle1 = new Circle(1, 2, 3, vectorAPI);
Shape circle2 = new Circle(5, 7, 11, rasterAPI);
Shape rectangle1 = new Rectangle(2, 3, 4, 5, vectorAPI);
Shape rectangle2 = new Rectangle(6, 7, 8, 9, rasterAPI);
circle1.draw();
circle2.draw();
rectangle1.draw();
rectangle2.draw();
circle1.resize(2);
circle1.draw();
}
}
2. Real-World Example: Notification System
Notification Bridge Implementation
import java.util.List;
// Implementation interface for message sending
interface MessageSender {
void sendMessage(String recipient, String subject, String body);
void sendBulkMessage(List<String> recipients, String subject, String body);
boolean supportsAttachment();
}
// Concrete implementations
class EmailSender implements MessageSender {
@Override
public void sendMessage(String recipient, String subject, String body) {
System.out.printf("Sending EMAIL to: %s\nSubject: %s\nBody: %s\n\n",
recipient, subject, body);
}
@Override
public void sendBulkMessage(List<String> recipients, String subject, String body) {
System.out.printf("Sending BULK EMAIL to %d recipients\nSubject: %s\n\n",
recipients.size(), subject);
recipients.forEach(recipient ->
System.out.printf(" - Sent to: %s\n", recipient));
}
@Override
public boolean supportsAttachment() {
return true;
}
}
class SMSSender implements MessageSender {
@Override
public void sendMessage(String recipient, String subject, String body) {
// SMS typically doesn't have subjects, so we combine subject and body
String smsContent = subject + ": " + body;
if (smsContent.length() > 160) {
smsContent = smsContent.substring(0, 157) + "...";
}
System.out.printf("Sending SMS to: %s\nMessage: %s\n\n", recipient, smsContent);
}
@Override
public void sendBulkMessage(List<String> recipients, String subject, String body) {
System.out.printf("Sending BULK SMS to %d recipients\n", recipients.size());
recipients.forEach(recipient ->
System.out.printf(" - Sent to: %s\n", recipient));
}
@Override
public boolean supportsAttachment() {
return false;
}
}
class PushNotificationSender implements MessageSender {
@Override
public void sendMessage(String recipient, String subject, String body) {
System.out.printf("Sending PUSH NOTIFICATION to device: %s\nTitle: %s\nBody: %s\n\n",
recipient, subject, body);
}
@Override
public void sendBulkMessage(List<String> recipients, String subject, String body) {
System.out.printf("Sending BULK PUSH to %d devices\nTitle: %s\n\n",
recipients.size(), subject);
}
@Override
public boolean supportsAttachment() {
return true;
}
}
// Abstraction - Notification
abstract class Notification {
protected MessageSender messageSender;
protected String recipient;
protected String message;
public Notification(MessageSender messageSender, String recipient, String message) {
this.messageSender = messageSender;
this.recipient = recipient;
this.message = message;
}
public abstract void send();
public abstract void setPriority(String priority);
// Template method for common preprocessing
public final void prepareAndSend() {
validateRecipient();
formatMessage();
send();
logNotification();
}
protected void validateRecipient() {
if (recipient == null || recipient.trim().isEmpty()) {
throw new IllegalArgumentException("Recipient cannot be null or empty");
}
}
protected void formatMessage() {
this.message = message.trim();
}
protected void logNotification() {
System.out.printf("Notification sent to %s using %s\n",
recipient, messageSender.getClass().getSimpleName());
}
}
// Refined abstractions
class AlertNotification extends Notification {
private String severity;
private String source;
public AlertNotification(MessageSender messageSender, String recipient,
String message, String severity, String source) {
super(messageSender, recipient, message);
this.severity = severity;
this.source = source;
}
@Override
public void send() {
String subject = String.format("[%s ALERT] from %s", severity.toUpperCase(), source);
messageSender.sendMessage(recipient, subject, message);
}
@Override
public void setPriority(String priority) {
this.severity = priority;
}
public void acknowledge() {
System.out.printf("Alert from %s acknowledged by %s\n", source, recipient);
}
}
class PromotionalNotification extends Notification {
private String campaign;
private String discountCode;
private boolean hasAttachment;
public PromotionalNotification(MessageSender messageSender, String recipient,
String message, String campaign, String discountCode) {
super(messageSender, recipient, message);
this.campaign = campaign;
this.discountCode = discountCode;
}
@Override
public void send() {
String subject = String.format("🎉 %s - Special Offer!", campaign);
String fullMessage = String.format("%s\n\nUse code: %s", message, discountCode);
messageSender.sendMessage(recipient, subject, fullMessage);
if (hasAttachment && messageSender.supportsAttachment()) {
System.out.println("📎 Attachment included: promotional_brochure.pdf");
}
}
@Override
public void setPriority(String priority) {
// Promotional notifications typically don't have priority levels
System.out.println("Priority setting not applicable for promotional notifications");
}
public void setHasAttachment(boolean hasAttachment) {
this.hasAttachment = hasAttachment;
if (hasAttachment && !messageSender.supportsAttachment()) {
System.out.println("Warning: Selected sender doesn't support attachments");
}
}
}
class SystemNotification extends Notification {
private String systemComponent;
private String notificationType;
public SystemNotification(MessageSender messageSender, String recipient,
String message, String systemComponent, String notificationType) {
super(messageSender, recipient, message);
this.systemComponent = systemComponent;
this.notificationType = notificationType;
}
@Override
public void send() {
String subject = String.format("[%s] %s Notification",
systemComponent, notificationType);
messageSender.sendMessage(recipient, subject, message);
}
@Override
public void setPriority(String priority) {
this.notificationType = priority;
}
}
// Client code
public class NotificationSystemDemo {
public static void main(String[] args) {
// Create different message senders
MessageSender emailSender = new EmailSender();
MessageSender smsSender = new SMSSender();
MessageSender pushSender = new PushNotificationSender();
// Create different types of notifications with different senders
Notification alertEmail = new AlertNotification(
emailSender, "[email protected]",
"Database connection pool is at 95% capacity", "HIGH", "Database Monitor"
);
Notification promoSM = new PromotionalNotification(
smsSender, "+1234567890",
"Get 20% off on all products this weekend!", "Weekend Sale", "WEEKEND20"
);
((PromotionalNotification) promoSM).setHasAttachment(false);
Notification systemPush = new SystemNotification(
pushSender, "user_device_123",
"System maintenance scheduled for tonight at 2 AM", "Infrastructure", "MAINTENANCE"
);
// Send notifications
System.out.println("=== Sending Notifications ===");
alertEmail.prepareAndSend();
promoSM.prepareAndSend();
systemPush.prepareAndSend();
// Demonstrate bulk sending
System.out.println("=== Bulk Notification Example ===");
List<String> recipients = List.of("[email protected]", "[email protected]", "[email protected]");
emailSender.sendBulkMessage(recipients, "System Update", "Please update your app to the latest version");
// Show flexibility - same notification type with different senders
System.out.println("=== Same Alert, Different Senders ===");
Notification criticalAlert1 = new AlertNotification(
emailSender, "[email protected]", "CRITICAL: Server down!", "CRITICAL", "Server Monitor"
);
Notification criticalAlert2 = new AlertNotification(
smsSender, "+1987654321", "CRITICAL: Server down!", "CRITICAL", "Server Monitor"
);
criticalAlert1.prepareAndSend();
criticalAlert2.prepareAndSend();
}
}
3. Database Abstraction Bridge
Database Bridge Pattern
import java.sql.*;
import java.util.*;
import java.util.Date;
// Implementation interface for database operations
interface DatabaseImplementation {
Connection connect(String url, String username, String password) throws SQLException;
void disconnect(Connection connection) throws SQLException;
ResultSet executeQuery(Connection connection, String query) throws SQLException;
int executeUpdate(Connection connection, String query) throws SQLException;
boolean supportsTransaction();
String getDatabaseType();
}
// Concrete implementations for different databases
class MySQLImplementation implements DatabaseImplementation {
@Override
public Connection connect(String url, String username, String password) throws SQLException {
String fullUrl = "jdbc:mysql://" + url;
return DriverManager.getConnection(fullUrl, username, password);
}
@Override
public void disconnect(Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public ResultSet executeQuery(Connection connection, String query) throws SQLException {
Statement statement = connection.createStatement();
return statement.executeQuery(query);
}
@Override
public int executeUpdate(Connection connection, String query) throws SQLException {
try (Statement statement = connection.createStatement()) {
return statement.executeUpdate(query);
}
}
@Override
public boolean supportsTransaction() {
return true;
}
@Override
public String getDatabaseType() {
return "MySQL";
}
}
class PostgreSQLImplementation implements DatabaseImplementation {
@Override
public Connection connect(String url, String username, String password) throws SQLException {
String fullUrl = "jdbc:postgresql://" + url;
return DriverManager.getConnection(fullUrl, username, password);
}
@Override
public void disconnect(Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public ResultSet executeQuery(Connection connection, String query) throws SQLException {
Statement statement = connection.createStatement();
return statement.executeQuery(query);
}
@Override
public int executeUpdate(Connection connection, String query) throws SQLException {
try (Statement statement = connection.createStatement()) {
return statement.executeUpdate(query);
}
}
@Override
public boolean supportsTransaction() {
return true;
}
@Override
public String getDatabaseType() {
return "PostgreSQL";
}
}
class SQLiteImplementation implements DatabaseImplementation {
@Override
public Connection connect(String url, String username, String password) throws SQLException {
// SQLite doesn't use username/password
String fullUrl = "jdbc:sqlite:" + url;
return DriverManager.getConnection(fullUrl);
}
@Override
public void disconnect(Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
@Override
public ResultSet executeQuery(Connection connection, String query) throws SQLException {
Statement statement = connection.createStatement();
return statement.executeQuery(query);
}
@Override
public int executeUpdate(Connection connection, String query) throws SQLException {
try (Statement statement = connection.createStatement()) {
return statement.executeUpdate(query);
}
}
@Override
public boolean supportsTransaction() {
return true;
}
@Override
public String getDatabaseType() {
return "SQLite";
}
}
// Abstraction - Database Accessor
abstract class DatabaseAccessor {
protected DatabaseImplementation implementation;
protected Connection connection;
protected String url;
protected String username;
protected String password;
public DatabaseAccessor(DatabaseImplementation implementation,
String url, String username, String password) {
this.implementation = implementation;
this.url = url;
this.username = username;
this.password = password;
}
public void connect() throws SQLException {
this.connection = implementation.connect(url, username, password);
System.out.println("Connected to " + implementation.getDatabaseType());
}
public void disconnect() throws SQLException {
if (connection != null) {
implementation.disconnect(connection);
System.out.println("Disconnected from " + implementation.getDatabaseType());
}
}
public abstract List<Map<String, Object>> executeQuery(String query) throws SQLException;
public abstract int executeUpdate(String query) throws SQLException;
public abstract void beginTransaction() throws SQLException;
public abstract void commitTransaction() throws SQLException;
public abstract void rollbackTransaction() throws SQLException;
// Template method
public final <T> T executeInTransaction(TransactionalOperation<T> operation) throws SQLException {
boolean originalAutoCommit = connection.getAutoCommit();
try {
connection.setAutoCommit(false);
beginTransaction();
T result = operation.execute();
commitTransaction();
return result;
} catch (SQLException e) {
rollbackTransaction();
throw e;
} finally {
connection.setAutoCommit(originalAutoCommit);
}
}
@FunctionalInterface
public interface TransactionalOperation<T> {
T execute() throws SQLException;
}
}
// Refined abstractions
class UserDatabaseAccessor extends DatabaseAccessor {
public UserDatabaseAccessor(DatabaseImplementation implementation,
String url, String username, String password) {
super(implementation, url, username, password);
}
@Override
public List<Map<String, Object>> executeQuery(String query) throws SQLException {
List<Map<String, Object>> results = new ArrayList<>();
try (ResultSet resultSet = implementation.executeQuery(connection, query)) {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
row.put(metaData.getColumnName(i), resultSet.getObject(i));
}
results.add(row);
}
}
return results;
}
@Override
public int executeUpdate(String query) throws SQLException {
return implementation.executeUpdate(connection, query);
}
@Override
public void beginTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
System.out.println("Beginning transaction...");
}
}
@Override
public void commitTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
connection.commit();
System.out.println("Transaction committed");
}
}
@Override
public void rollbackTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
connection.rollback();
System.out.println("Transaction rolled back");
}
}
// User-specific operations
public void createUser(String username, String email) throws SQLException {
String query = String.format(
"INSERT INTO users (username, email, created_at) VALUES ('%s', '%s', NOW())",
username, email
);
int affectedRows = executeUpdate(query);
System.out.println("Created user: " + username + " (" + affectedRows + " rows affected)");
}
public List<Map<String, Object>> getAllUsers() throws SQLException {
return executeQuery("SELECT * FROM users ORDER BY created_at DESC");
}
}
class ProductDatabaseAccessor extends DatabaseAccessor {
public ProductDatabaseAccessor(DatabaseImplementation implementation,
String url, String username, String password) {
super(implementation, url, username, password);
}
@Override
public List<Map<String, Object>> executeQuery(String query) throws SQLException {
// Could add product-specific query processing
List<Map<String, Object>> results = new ArrayList<>();
try (ResultSet resultSet = implementation.executeQuery(connection, query)) {
ResultSetMetaData metaData = resultSet.getMetaData();
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
Map<String, Object> row = new LinkedHashMap<>();
for (int i = 1; i <= columnCount; i++) {
row.put(metaData.getColumnName(i), resultSet.getObject(i));
}
results.add(row);
}
}
return results;
}
@Override
public int executeUpdate(String query) throws SQLException {
return implementation.executeUpdate(connection, query);
}
@Override
public void beginTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
System.out.println("Beginning product transaction...");
}
}
@Override
public void commitTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
connection.commit();
System.out.println("Product transaction committed");
}
}
@Override
public void rollbackTransaction() throws SQLException {
if (implementation.supportsTransaction()) {
connection.rollback();
System.out.println("Product transaction rolled back");
}
}
// Product-specific operations
public void updateProductPrice(int productId, double newPrice) throws SQLException {
String query = String.format(
"UPDATE products SET price = %.2f, updated_at = NOW() WHERE id = %d",
newPrice, productId
);
executeInTransaction(() -> {
int affected = executeUpdate(query);
System.out.println("Updated price for product " + productId + " to $" + newPrice);
return affected;
});
}
}
// Client code
public class DatabaseBridgeDemo {
public static void main(String[] args) {
try {
// Different database implementations
DatabaseImplementation mysql = new MySQLImplementation();
DatabaseImplementation postgresql = new PostgreSQLImplementation();
DatabaseImplementation sqlite = new SQLiteImplementation();
// Same abstraction with different implementations
UserDatabaseAccessor mysqlUserAccessor = new UserDatabaseAccessor(
mysql, "localhost:3306/mydb", "root", "password"
);
UserDatabaseAccessor sqliteUserAccessor = new UserDatabaseAccessor(
sqlite, "test.db", "", ""
);
ProductDatabaseAccessor postgresProductAccessor = new ProductDatabaseAccessor(
postgresql, "localhost:5432/products", "admin", "secret"
);
// Demonstrate usage
System.out.println("=== MySQL User Operations ===");
mysqlUserAccessor.connect();
mysqlUserAccessor.createUser("john_doe", "[email protected]");
// This would actually work with a real database
// List<Map<String, Object>> users = mysqlUserAccessor.getAllUsers();
// System.out.println("Users: " + users);
mysqlUserAccessor.disconnect();
System.out.println("\n=== SQLite User Operations ===");
sqliteUserAccessor.connect();
sqliteUserAccessor.createUser("jane_smith", "[email protected]");
sqliteUserAccessor.disconnect();
System.out.println("\n=== PostgreSQL Product Operations ===");
postgresProductAccessor.connect();
postgresProductAccessor.updateProductPrice(1, 29.99);
postgresProductAccessor.disconnect();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. Advanced Bridge Pattern with Factory
Bridge Pattern with Factory Method
import java.util.HashMap;
import java.util.Map;
// Implementation Registry
class ImplementationRegistry {
private static final Map<String, MessageSender> senders = new HashMap<>();
static {
senders.put("EMAIL", new EmailSender());
senders.put("SMS", new SMSSender());
senders.put("PUSH", new PushNotificationSender());
}
public static MessageSender getSender(String type) {
MessageSender sender = senders.get(type.toUpperCase());
if (sender == null) {
throw new IllegalArgumentException("Unknown sender type: " + type);
}
return sender;
}
public static void registerSender(String type, MessageSender sender) {
senders.put(type.toUpperCase(), sender);
}
}
// Notification Factory using Bridge Pattern
class NotificationFactory {
public static Notification createAlertNotification(String senderType, String recipient,
String message, String severity, String source) {
MessageSender sender = ImplementationRegistry.getSender(senderType);
return new AlertNotification(sender, recipient, message, severity, source);
}
public static Notification createPromotionalNotification(String senderType, String recipient,
String message, String campaign, String discountCode) {
MessageSender sender = ImplementationRegistry.getSender(senderType);
return new PromotionalNotification(sender, recipient, message, campaign, discountCode);
}
public static Notification createSystemNotification(String senderType, String recipient,
String message, String component, String type) {
MessageSender sender = ImplementationRegistry.getSender(senderType);
return new SystemNotification(sender, recipient, message, component, type);
}
}
// Advanced client usage
public class AdvancedBridgeDemo {
public static void main(String[] args) {
// Register a custom sender
ImplementationRegistry.registerSender("SLACK", new SlackSender());
System.out.println("=== Factory with Bridge Pattern ===");
// Create notifications using factory
Notification alert = NotificationFactory.createAlertNotification(
"EMAIL", "[email protected]", "Server load high", "MEDIUM", "Load Balancer"
);
Notification promo = NotificationFactory.createPromotionalNotification(
"SMS", "+1234567890", "New features available!", "Product Update", "UPDATE2024"
);
Notification system = NotificationFactory.createSystemNotification(
"SLACK", "#alerts", "Deployment completed", "CI/CD", "DEPLOYMENT"
);
// Send all notifications
alert.prepareAndSend();
promo.prepareAndSend();
system.prepareAndSend();
}
}
// Custom sender implementation
class SlackSender implements MessageSender {
@Override
public void sendMessage(String recipient, String subject, String body) {
System.out.printf("Sending SLACK message to channel: %s\nSubject: %s\nBody: %s\n\n",
recipient, subject, body);
}
@Override
public void sendBulkMessage(List<String> recipients, String subject, String body) {
System.out.printf("Sending BULK SLACK to %d channels\nSubject: %s\n\n",
recipients.size(), subject);
}
@Override
public boolean supportsAttachment() {
return true;
}
}
Key Benefits of Bridge Pattern
- Decoupling: Separates abstraction from implementation
- Extensibility: Both abstractions and implementations can be extended independently
- Single Responsibility: Each class has a clear, focused purpose
- Open/Closed Principle: Easy to add new abstractions or implementations
- Runtime Binding: Implementation can be selected at runtime
- Avoids Cartesian Product: Prevents class explosion when you have multiple dimensions of variation
When to Use Bridge Pattern
- When you want to avoid permanent binding between abstraction and implementation
- When both abstractions and implementations should be extensible via subclassing
- When changes in implementation should not affect clients
- When you want to share an implementation among multiple objects
- When you need to switch implementations at runtime
This comprehensive guide demonstrates how the Bridge Pattern enables clean separation of concerns and provides flexibility in designing complex systems.