Factory Design Pattern using C# with a real world banking example

7 minute read

The Factory Design Pattern is one of the most fundamental and widely-used creational patterns in software development. It mirrors the concept of real-world factories by centralizing object creation logic and abstracting the instantiation process from client code.

What is the Factory Design Pattern?

The Factory Pattern is a creational design pattern that provides an interface for creating objects without specifying their exact classes. Instead of directly instantiating objects using the new keyword, clients delegate this responsibility to a factory class.

The pattern involves three main actors:

  • Client: The object that needs another object for specific purposes
  • Factory: The class responsible for creating instances
  • Product: The objects being created (typically implementing a common interface)

Factory Pattern Process

Real-World Application: Banking System

Let’s explore the Factory Pattern through a banking application that manages different types of accounts. In this system:

  • Multiple account types exist (Savings Account, Checking Account)
  • All accounts implement a common interface (IAccount)
  • Clients need account instances without knowing implementation details
  • New account types can be added without modifying existing client code

When a client needs to know the interest rate for a Savings Account, it simply requests the factory to create an instance. The factory handles the instantiation logic, returns the appropriate account type, and the client works with the object through the common interface—completely unaware of the concrete implementation.

This approach provides several key advantages:

  • Centralized object creation: All instantiation logic is located in one place
  • Loose coupling: Clients depend only on interfaces, not concrete classes
  • Easy extensibility: New account types can be added without modifying client code

Class Diagram

The following class diagram illustrates the structure of our Factory Pattern implementation:

Key Relationships:

  • Inheritance: All concrete account classes implement the IAccount interface
  • Dependency: AccountFactory depends on AccountType enum and creates IAccount instances
  • Usage: BankingApplication (client) uses the factory to obtain accounts and works with them through the interface
  • Creation: The factory creates concrete instances but returns them as interface references

This diagram shows how the Factory Pattern decouples the client code from concrete implementations, allowing for easy extension and maintenance.

Key Benefits

  • Most widely adopted pattern: Extensively used across enterprise applications
  • Loose coupling: Eliminates tight binding between client code and specific implementations
  • Interface-based design: Client code works exclusively with interfaces
  • Flexible object creation: Supports multiple instances and object variations
  • Extensibility: Enables subclassing and polymorphic behavior
  • Minimal coupling: Connects class hierarchies with reduced dependencies
  • Future-proof: Product implementations can evolve without affecting clients

Implementation Guide

Let’s build our banking system step by step to demonstrate the Factory Pattern in action.

Step 1: Define the Product Interface

First, we create the IAccount interface that defines the contract for all account types:

using System;

namespace FactoryPattern
{
    /// <summary>
    /// Common interface for all account types
    /// Defines the contract that all account implementations must follow
    /// </summary>
    public interface IAccount
    {
        string Withdraw(decimal amount);
        string Deposit(decimal amount);
        double GetInterestRate();
        string GetAccountType();
    }
}

Step 2: Implement Concrete Products

Now we’ll create concrete implementations of different account types.

Savings Account Implementation

using System;

namespace FactoryPattern
{
    /// <summary>
    /// Concrete implementation of a Savings Account
    /// Provides higher interest rates but may have withdrawal limitations
    /// </summary>
    public class SavingsAccount : IAccount
    {
        private decimal balance = 0;
        
        public string Withdraw(decimal amount)
        {
            if (amount <= balance)
            {
                balance -= amount;
                return $"Withdrew ${amount}. New balance: ${balance}";
            }
            return "Insufficient funds for withdrawal.";
        }
        
        public string Deposit(decimal amount)
        {
            balance += amount;
            return $"Deposited ${amount}. New balance: ${balance}";
        }
        
        public double GetInterestRate()
        {
            return 2.5; // 2.5% interest rate for savings
        }
        
        public string GetAccountType()
        {
            return "Savings Account";
        }
    }
}

Checking Account Implementation

using System;

namespace FactoryPattern
{
    /// <summary>
    /// Concrete implementation of a Checking Account
    /// Provides easier access to funds but typically lower interest rates
    /// </summary>
    public class CheckingAccount : IAccount
    {
        private decimal balance = 0;
        
        public string Withdraw(decimal amount)
        {
            // Checking accounts might allow overdrafts
            balance -= amount;
            return $"Withdrew ${amount}. New balance: ${balance}";
        }
        
        public string Deposit(decimal amount)
        {
            balance += amount;
            return $"Deposited ${amount}. New balance: ${balance}";
        }
        
        public double GetInterestRate()
        {
            return 0.1; // 0.1% interest rate for checking
        }
        
        public string GetAccountType()
        {
            return "Checking Account";
        }
    }
}

Step 3: Create Account Type Enumeration

Define an enumeration to specify which type of account to create:

namespace FactoryPattern
{
    /// <summary>
    /// Enumeration defining the types of accounts available
    /// Used by the factory to determine which concrete class to instantiate
    /// </summary>
    public enum AccountType
    {
        Savings,
        Checking,
        BusinessChecking // Example of easy extensibility
    }
}

Step 4: Implement the Factory Class

The factory class contains the core logic for object creation:

using System;

namespace FactoryPattern
{
    /// <summary>
    /// Factory class responsible for creating account instances
    /// Centralizes object creation logic and provides a single point of control
    /// </summary>
    public static class AccountFactory
    {
        /// <summary>
        /// Creates an account instance based on the specified type
        /// </summary>
        /// <param name="accountType">The type of account to create</param>
        /// <returns>An IAccount instance of the specified type</returns>
        /// <exception cref="ArgumentException">Thrown when an invalid account type is provided</exception>
        public static IAccount CreateAccount(AccountType accountType)
        {
            return accountType switch
            {
                AccountType.Savings => new SavingsAccount(),
                AccountType.Checking => new CheckingAccount(),
                AccountType.BusinessChecking => new BusinessCheckingAccount(), // Future extension
                _ => throw new ArgumentException($"Unknown account type: {accountType}")
            };
        }
        
        /// <summary>
        /// Overloaded method that creates an account based on string input
        /// Useful for scenarios where account type comes from user input or configuration
        /// </summary>
        /// <param name="accountTypeName">String representation of the account type</param>
        /// <returns>An IAccount instance of the specified type</returns>
        public static IAccount CreateAccount(string accountTypeName)
        {
            if (Enum.TryParse<AccountType>(accountTypeName, true, out AccountType accountType))
            {
                return CreateAccount(accountType);
            }
            
            throw new ArgumentException($"Invalid account type name: {accountTypeName}");
        }
    }
}

Step 5: Client Implementation and Usage

Finally, let’s see how the client code uses the factory:

using System;

namespace FactoryPattern
{
    /// <summary>
    /// Client application demonstrating the Factory Pattern usage
    /// Notice how the client code is completely decoupled from concrete implementations
    /// </summary>
    public class BankingApplication
    {
        public static void Main(string[] args)
        {
            Console.WriteLine("=== Banking System - Factory Pattern Demo ===\n");
            
            try
            {
                // Create different types of accounts using the factory
                IAccount savingsAccount = AccountFactory.CreateAccount(AccountType.Savings);
                IAccount checkingAccount = AccountFactory.CreateAccount(AccountType.Checking);
                
                // Alternative: Create account using string input (useful for user interfaces)
                IAccount userAccount = AccountFactory.CreateAccount("Savings");
                
                // Use the accounts without knowing their concrete types
                DisplayAccountInfo(savingsAccount);
                DisplayAccountInfo(checkingAccount);
                
                // Perform operations
                Console.WriteLine("\n=== Account Operations ===");
                Console.WriteLine(savingsAccount.Deposit(1000));
                Console.WriteLine(savingsAccount.Withdraw(250));
                
                Console.WriteLine(checkingAccount.Deposit(500));
                Console.WriteLine(checkingAccount.Withdraw(600)); // This might overdraft
                
            }
            catch (ArgumentException ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
            
            Console.WriteLine("\nPress any key to exit...");
            Console.ReadKey();
        }
        
        /// <summary>
        /// Helper method to display account information
        /// Demonstrates working with objects through their interface
        /// </summary>
        /// <param name="account">Account instance to display information for</param>
        private static void DisplayAccountInfo(IAccount account)
        {
            Console.WriteLine($"Account Type: {account.GetAccountType()}");
            Console.WriteLine($"Interest Rate: {account.GetInterestRate()}%");
            Console.WriteLine();
        }
    }
}

Extending the Pattern: Adding New Account Types

One of the Factory Pattern’s greatest strengths is its extensibility. Let’s add a new account type without modifying existing code:

namespace FactoryPattern
{
    /// <summary>
    /// New account type demonstrating pattern extensibility
    /// Added without modifying any existing client code
    /// </summary>
    public class BusinessCheckingAccount : IAccount
    {
        private decimal balance = 0;
        
        public string Withdraw(decimal amount)
        {
            // Business accounts might have different fee structures
            decimal fee = 2.50m; // Transaction fee
            decimal totalAmount = amount + fee;
            
            if (totalAmount <= balance)
            {
                balance -= totalAmount;
                return $"Withdrew ${amount} (${fee} fee). New balance: ${balance}";
            }
            return "Insufficient funds for withdrawal including fees.";
        }
        
        public string Deposit(decimal amount)
        {
            balance += amount;
            return $"Business deposit: ${amount}. New balance: ${balance}";
        }
        
        public double GetInterestRate()
        {
            return 0.05; // Lower rate but business-focused features
        }
        
        public string GetAccountType()
        {
            return "Business Checking Account";
        }
    }
}

To use this new account type, simply update the factory’s switch statement and the enumeration. No client code needs modification!

Best Practices and Considerations

When to Use the Factory Pattern

  • Multiple related classes need to be created
  • Object creation logic is complex or likely to change
  • Client code should be decoupled from specific implementations
  • Runtime decisions determine which class to instantiate

Design Considerations

  • Keep factories focused: Each factory should handle related object types
  • Use dependency injection: Consider integrating with DI containers for better testability
  • Handle errors gracefully: Provide clear error messages for invalid inputs
  • Document factory methods: Make it clear what each factory method produces

Advanced Variations

  • Abstract Factory: For creating families of related objects
  • Factory Method: Using inheritance to delegate object creation to subclasses
  • Registration-based Factory: Using reflection or registration mechanisms for dynamic type discovery

Conclusion

The Factory Design Pattern provides a robust solution for managing object creation while maintaining loose coupling and high extensibility. Through our banking system example, we’ve seen how this pattern:

  • Centralizes object creation logic
  • Enables easy addition of new types
  • Maintains clean separation of concerns
  • Simplifies client code

By implementing the Factory Pattern, your applications become more maintainable, testable, and ready for future enhancements. The pattern’s widespread adoption in enterprise applications speaks to its effectiveness in solving real-world object creation challenges.

Source code available on GitHub

Comments