What is Facade Pattern?
The Facade pattern is a structural design pattern that provides a simplified interface to a complex subsystem. It acts like a friendly receptionist at a large corporation - instead of navigating through multiple departments and complex procedures, you just tell the receptionist what you need, and they handle all the complexity behind the scenes.
I’ll demonstrate how this pattern can dramatically simplify client code and hide the complexity of intricate systems in Go applications.
Let’s start with a scenario: E-commerce Order Processing
Imagine you’re building an e-commerce platform where processing an order involves multiple complex subsystems: inventory management, payment processing, shipping calculation, notification services, and audit logging. Each subsystem has its own API with different parameters and error handling.
Without Facade Pattern
Here’s how client code might look without the Facade pattern:
// Client code has to deal with multiple complex subsystems
func ProcessOrderDirectly(orderData OrderData) error {
// Initialize all subsystems
inventoryService := inventory.NewService(inventoryConfig)
paymentService := payment.NewProcessor(paymentConfig)
shippingService := shipping.NewCalculator(shippingConfig)
notificationService := notification.NewService(notificationConfig)
auditService := audit.NewLogger(auditConfig)
// Check inventory - complex API
inventoryRequest := inventory.CheckRequest{
ProductID: orderData.ProductID,
Quantity: orderData.Quantity,
Warehouse: orderData.PreferredWarehouse,
}
available, err := inventoryService.CheckAvailability(inventoryRequest)
if err != nil {
return fmt.Errorf("inventory check failed: %w", err)
}
if !available {
return errors.New("product not available")
}
// Calculate shipping - another complex API
shippingRequest := shipping.CalculationRequest{
FromAddress: orderData.WarehouseAddress,
ToAddress: orderData.CustomerAddress,
Weight: orderData.ProductWeight,
Dimensions: orderData.ProductDimensions,
ServiceType: orderData.ShippingPreference,
}
shippingCost, err := shippingService.CalculateCost(shippingRequest)
if err != nil {
return fmt.Errorf("shipping calculation failed: %w", err)
}
// Process payment - yet another complex API
paymentRequest := payment.ProcessRequest{
Amount: orderData.ProductPrice + shippingCost,
Currency: orderData.Currency,
PaymentMethod: orderData.PaymentMethod,
CustomerID: orderData.CustomerID,
BillingInfo: orderData.BillingInfo,
}
paymentResult, err := paymentService.ProcessPayment(paymentRequest)
if err != nil {
return fmt.Errorf("payment processing failed: %w", err)
}
// Reserve inventory
reserveRequest := inventory.ReserveRequest{
ProductID: orderData.ProductID,
Quantity: orderData.Quantity,
TransactionID: paymentResult.TransactionID,
}
err = inventoryService.ReserveItems(reserveRequest)
if err != nil {
// Rollback payment
paymentService.RefundPayment(paymentResult.TransactionID)
return fmt.Errorf("inventory reservation failed: %w", err)
}
// Send notifications
notificationRequest := notification.OrderConfirmationRequest{
CustomerEmail: orderData.CustomerEmail,
OrderID: orderData.OrderID,
Items: orderData.Items,
TotalAmount: paymentRequest.Amount,
}
err = notificationService.SendOrderConfirmation(notificationRequest)
if err != nil {
// Log error but don't fail the order
log.Printf("Failed to send notification: %v", err)
}
// Audit logging
auditEntry := audit.Entry{
Action: "ORDER_PROCESSED",
UserID: orderData.CustomerID,
OrderID: orderData.OrderID,
Amount: paymentRequest.Amount,
Timestamp: time.Now(),
}
err = auditService.LogEntry(auditEntry)
if err != nil {
log.Printf("Failed to log audit entry: %v", err)
}
return nil
}
// This is overwhelming for client code!
With Facade Pattern
Let’s create a facade to simplify this complexity:
package main
import (
"fmt"
"log"
"time"
)
// Simple data structures for the example
type OrderData struct {
OrderID string
CustomerID string
CustomerEmail string
ProductID string
Quantity int
ProductPrice float64
Currency string
PaymentMethod string
ShippingPreference string
CustomerAddress Address
BillingInfo BillingInfo
}
type Address struct {
Street string
City string
State string
ZipCode string
Country string
}
type BillingInfo struct {
CardNumber string
ExpiryDate string
CVV string
}
type OrderResult struct {
OrderID string
TransactionID string
TotalAmount float64
EstimatedDelivery time.Time
TrackingNumber string
}
// Subsystem interfaces (simplified for example)
type InventoryService interface {
CheckAvailability(productID string, quantity int) (bool, error)
ReserveItems(productID string, quantity int, transactionID string) error
ReleaseReservation(productID string, quantity int, transactionID string) error
}
type PaymentService interface {
ProcessPayment(amount float64, paymentMethod string, billingInfo BillingInfo) (string, error)
RefundPayment(transactionID string) error
}
type ShippingService interface {
CalculateCost(fromAddr, toAddr Address, weight float64) (float64, error)
CreateShipment(orderID string, fromAddr, toAddr Address) (string, time.Time, error)
}
type NotificationService interface {
SendOrderConfirmation(email string, orderData OrderData, totalAmount float64) error
SendShippingNotification(email string, trackingNumber string) error
}
type AuditService interface {
LogOrderProcessed(orderID, customerID string, amount float64) error
}
// Concrete implementations (simplified)
type SimpleInventoryService struct{}
func (s *SimpleInventoryService) CheckAvailability(productID string, quantity int) (bool, error) {
// Simulate inventory check
log.Printf("Checking inventory for product %s, quantity %d", productID, quantity)
return true, nil
}
func (s *SimpleInventoryService) ReserveItems(productID string, quantity int, transactionID string) error {
log.Printf("Reserving %d items of product %s for transaction %s", quantity, productID, transactionID)
return nil
}
func (s *SimpleInventoryService) ReleaseReservation(productID string, quantity int, transactionID string) error {
log.Printf("Releasing reservation for %d items of product %s", quantity, productID)
return nil
}
type SimplePaymentService struct{}
func (s *SimplePaymentService) ProcessPayment(amount float64, paymentMethod string, billingInfo BillingInfo) (string, error) {
log.Printf("Processing payment of $%.2f using %s", amount, paymentMethod)
return fmt.Sprintf("txn_%d", time.Now().Unix()), nil
}
func (s *SimplePaymentService) RefundPayment(transactionID string) error {
log.Printf("Refunding payment for transaction %s", transactionID)
return nil
}
type SimpleShippingService struct{}
func (s *SimpleShippingService) CalculateCost(fromAddr, toAddr Address, weight float64) (float64, error) {
log.Printf("Calculating shipping cost from %s to %s", fromAddr.City, toAddr.City)
return 9.99, nil // Flat rate for example
}
func (s *SimpleShippingService) CreateShipment(orderID string, fromAddr, toAddr Address) (string, time.Time, error) {
trackingNumber := fmt.Sprintf("TRACK_%s_%d", orderID, time.Now().Unix())
estimatedDelivery := time.Now().Add(3 * 24 * time.Hour) // 3 days
log.Printf("Created shipment %s for order %s", trackingNumber, orderID)
return trackingNumber, estimatedDelivery, nil
}
type SimpleNotificationService struct{}
func (s *SimpleNotificationService) SendOrderConfirmation(email string, orderData OrderData, totalAmount float64) error {
log.Printf("Sending order confirmation to %s for order %s (total: $%.2f)", email, orderData.OrderID, totalAmount)
return nil
}
func (s *SimpleNotificationService) SendShippingNotification(email string, trackingNumber string) error {
log.Printf("Sending shipping notification to %s with tracking %s", email, trackingNumber)
return nil
}
type SimpleAuditService struct{}
func (s *SimpleAuditService) LogOrderProcessed(orderID, customerID string, amount float64) error {
log.Printf("AUDIT: Order %s processed for customer %s, amount $%.2f", orderID, customerID, amount)
return nil
}
// FACADE - This is the key part!
type OrderProcessingFacade struct {
inventoryService InventoryService
paymentService PaymentService
shippingService ShippingService
notificationService NotificationService
auditService AuditService
}
func NewOrderProcessingFacade() *OrderProcessingFacade {
return &OrderProcessingFacade{
inventoryService: &SimpleInventoryService{},
paymentService: &SimplePaymentService{},
shippingService: &SimpleShippingService{},
notificationService: &SimpleNotificationService{},
auditService: &SimpleAuditService{},
}
}
// Simple interface for clients
func (f *OrderProcessingFacade) ProcessOrder(orderData OrderData) (*OrderResult, error) {
log.Printf("Starting order processing for order %s", orderData.OrderID)
// Step 1: Check inventory
available, err := f.inventoryService.CheckAvailability(orderData.ProductID, orderData.Quantity)
if err != nil {
return nil, fmt.Errorf("inventory check failed: %w", err)
}
if !available {
return nil, fmt.Errorf("product %s not available in requested quantity", orderData.ProductID)
}
// Step 2: Calculate shipping
shippingCost, err := f.shippingService.CalculateCost(
Address{City: "Warehouse"}, // Simplified
orderData.CustomerAddress,
2.5, // Simplified weight
)
if err != nil {
return nil, fmt.Errorf("shipping calculation failed: %w", err)
}
totalAmount := orderData.ProductPrice + shippingCost
// Step 3: Process payment
transactionID, err := f.paymentService.ProcessPayment(totalAmount, orderData.PaymentMethod, orderData.BillingInfo)
if err != nil {
return nil, fmt.Errorf("payment processing failed: %w", err)
}
// Step 4: Reserve inventory
err = f.inventoryService.ReserveItems(orderData.ProductID, orderData.Quantity, transactionID)
if err != nil {
// Rollback payment
f.paymentService.RefundPayment(transactionID)
return nil, fmt.Errorf("inventory reservation failed: %w", err)
}
// Step 5: Create shipment
trackingNumber, estimatedDelivery, err := f.shippingService.CreateShipment(
orderData.OrderID,
Address{City: "Warehouse"},
orderData.CustomerAddress,
)
if err != nil {
// Rollback inventory and payment
f.inventoryService.ReleaseReservation(orderData.ProductID, orderData.Quantity, transactionID)
f.paymentService.RefundPayment(transactionID)
return nil, fmt.Errorf("shipment creation failed: %w", err)
}
// Step 6: Send notifications (non-critical)
err = f.notificationService.SendOrderConfirmation(orderData.CustomerEmail, orderData, totalAmount)
if err != nil {
log.Printf("Warning: Failed to send order confirmation: %v", err)
}
err = f.notificationService.SendShippingNotification(orderData.CustomerEmail, trackingNumber)
if err != nil {
log.Printf("Warning: Failed to send shipping notification: %v", err)
}
// Step 7: Audit logging (non-critical)
err = f.auditService.LogOrderProcessed(orderData.OrderID, orderData.CustomerID, totalAmount)
if err != nil {
log.Printf("Warning: Failed to log audit entry: %v", err)
}
log.Printf("Order %s processed successfully", orderData.OrderID)
return &OrderResult{
OrderID: orderData.OrderID,
TransactionID: transactionID,
TotalAmount: totalAmount,
EstimatedDelivery: estimatedDelivery,
TrackingNumber: trackingNumber,
}, nil
}
// Additional convenience methods
func (f *OrderProcessingFacade) CancelOrder(orderID, transactionID string) error {
log.Printf("Cancelling order %s", orderID)
// Simplified cancellation process
err := f.paymentService.RefundPayment(transactionID)
if err != nil {
return fmt.Errorf("refund failed: %w", err)
}
// Additional cleanup would go here
return nil
}
func (f *OrderProcessingFacade) GetOrderStatus(orderID string) (string, error) {
// Simplified status check
return "PROCESSED", nil
}
func main() {
// Client code is now much simpler!
facade := NewOrderProcessingFacade()
orderData := OrderData{
OrderID: "ORD-001",
CustomerID: "CUST-123",
CustomerEmail: "[email protected]",
ProductID: "PROD-456",
Quantity: 2,
ProductPrice: 29.99,
Currency: "USD",
PaymentMethod: "credit_card",
CustomerAddress: Address{
Street: "123 Main St",
City: "Anytown",
State: "CA",
ZipCode: "12345",
Country: "USA",
},
BillingInfo: BillingInfo{
CardNumber: "****-****-****-1234",
ExpiryDate: "12/25",
CVV: "123",
},
}
// Simple one-line order processing!
result, err := facade.ProcessOrder(orderData)
if err != nil {
log.Fatalf("Order processing failed: %v", err)
}
fmt.Printf("Order processed successfully!\n")
fmt.Printf("Order ID: %s\n", result.OrderID)
fmt.Printf("Transaction ID: %s\n", result.TransactionID)
fmt.Printf("Total Amount: $%.2f\n", result.TotalAmount)
fmt.Printf("Tracking Number: %s\n", result.TrackingNumber)
fmt.Printf("Estimated Delivery: %s\n", result.EstimatedDelivery.Format("2006-01-02"))
}
Advanced Facade with Configuration
For more flexibility, you can create configurable facades:
type FacadeConfig struct {
EnableNotifications bool
EnableAuditLogging bool
RetryAttempts int
TimeoutDuration time.Duration
}
type ConfigurableFacade struct {
*OrderProcessingFacade
config FacadeConfig
}
func NewConfigurableFacade(config FacadeConfig) *ConfigurableFacade {
return &ConfigurableFacade{
OrderProcessingFacade: NewOrderProcessingFacade(),
config: config,
}
}
func (f *ConfigurableFacade) ProcessOrder(orderData OrderData) (*OrderResult, error) {
// Use configuration to control behavior
if !f.config.EnableNotifications {
// Skip notification steps
}
if !f.config.EnableAuditLogging {
// Skip audit logging
}
// Implement retry logic based on config.RetryAttempts
// Implement timeout based on config.TimeoutDuration
return f.OrderProcessingFacade.ProcessOrder(orderData)
}
Real-world Use Cases
Here’s where I commonly use the Facade pattern in Go:
- API Clients: Simplifying complex third-party API interactions
- Database Operations: Hiding complex query logic behind simple methods
- File Processing: Combining multiple file operations into single calls
- Microservice Orchestration: Coordinating calls to multiple services
- Configuration Management: Simplifying complex configuration loading and validation
Benefits of Facade Pattern
- Simplicity: Provides a simple interface to complex subsystems
- Decoupling: Clients don’t need to know about subsystem details
- Flexibility: Can change subsystem implementations without affecting clients
- Centralized Logic: Business logic is centralized in the facade
- Error Handling: Consistent error handling across the system
Caveats
While the Facade pattern is helpful, consider these limitations:
- God Object Risk: Facades can become too large and do too much
- Hidden Complexity: May hide important details that clients need to know
- Performance: Additional layer can add overhead
- Tight Coupling: Facade becomes tightly coupled to all subsystems
- Limited Flexibility: May not expose all subsystem capabilities
Thank you
Thank you for reading! The Facade pattern is excellent for simplifying complex systems and providing clean APIs to clients. It’s particularly useful in Go when dealing with multiple packages or external services that need to work together. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!