Collections in Java Design Patterns – Leveraging Lists, Maps, and Sets in Factory, Strategy, and More

Illustration for Collections in Java Design Patterns – Leveraging Lists, Maps, and Sets in Factory, Strategy, and More
By Last updated:

Java Design Patterns offer time-tested solutions to common software design problems — and Java Collections like List, Map, and Set often play an integral role in implementing them effectively.

Whether you're managing a pool of strategies, mapping commands to handlers, or observing event listeners, understanding how collections enhance these patterns makes your code cleaner, more scalable, and maintainable.

This guide walks you through how to use collections in popular design patterns like Factory, Strategy, Observer, Decorator, and Composite, with real-world Java examples.


Why Collections Are Central to Design Patterns

Collections provide:

  • Flexible structure for registering and storing components
  • Efficient lookup and dispatching with Map
  • Order-preserving or deduplicating behavior via List or Set
  • Dynamic composition of multiple implementations

Factory Pattern + Map

Problem

You need to instantiate different objects based on input type.

Collection-Based Solution

Use a Map<String, Supplier<Product>> for lookup-based creation.

Example

interface Product {
    void deliver();
}

class Book implements Product {
    public void deliver() { System.out.println("Delivering book..."); }
}

class Laptop implements Product {
    public void deliver() { System.out.println("Delivering laptop..."); }
}

class ProductFactory {
    private static final Map<String, Supplier<Product>> registry = new HashMap<>();

    static {
        registry.put("book", Book::new);
        registry.put("laptop", Laptop::new);
    }

    public static Product create(String type) {
        return registry.getOrDefault(type, () -> () -> System.out.println("Unknown")).get();
    }
}

Strategy Pattern + Map or List

Problem

You need to select an algorithm at runtime.

Collection-Based Solution

  • Use a Map<String, Strategy> for named strategies
  • Use List<Strategy> for composite or ordered strategies

Example

interface PaymentStrategy {
    void pay(double amount);
}

class PayPal implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Paid via PayPal: " + amount); }
}

class CreditCard implements PaymentStrategy {
    public void pay(double amount) { System.out.println("Paid via Credit Card: " + amount); }
}

class PaymentProcessor {
    private final Map<String, PaymentStrategy> strategies = Map.of(
        "paypal", new PayPal(),
        "card", new CreditCard()
    );

    public void process(String method, double amount) {
        strategies.getOrDefault(method, a -> System.out.println("Invalid")).pay(amount);
    }
}

Observer Pattern + List

Problem

Notify multiple listeners when an event occurs.

Collection-Based Solution

Use List<Observer> to track all subscribers.

interface Observer {
    void update(String event);
}

class Logger implements Observer {
    public void update(String event) { System.out.println("Logged: " + event); }
}

class Notifier implements Observer {
    public void update(String event) { System.out.println("Notified user: " + event); }
}

class EventPublisher {
    private final List<Observer> observers = new ArrayList<>();

    public void subscribe(Observer observer) {
        observers.add(observer);
    }

    public void publish(String event) {
        observers.forEach(o -> o.update(event));
    }
}

Decorator Pattern + List

Track and apply decorators dynamically using a List.

interface Coffee {
    String serve();
}

class SimpleCoffee implements Coffee {
    public String serve() { return "Coffee"; }
}

class MilkDecorator implements Coffee {
    private final Coffee coffee;
    public MilkDecorator(Coffee coffee) { this.coffee = coffee; }
    public String serve() { return coffee.serve() + " + Milk"; }
}

class SugarDecorator implements Coffee {
    private final Coffee coffee;
    public SugarDecorator(Coffee coffee) { this.coffee = coffee; }
    public String serve() { return coffee.serve() + " + Sugar"; }
}

// Usage
Coffee coffee = new SugarDecorator(new MilkDecorator(new SimpleCoffee()));
System.out.println(coffee.serve()); // Coffee + Milk + Sugar

Composite Pattern + List

Structure hierarchies using List<Component> in each node.

interface FileSystemComponent {
    void display();
}

class FileLeaf implements FileSystemComponent {
    private final String name;
    public FileLeaf(String name) { this.name = name; }
    public void display() { System.out.println(name); }
}

class Folder implements FileSystemComponent {
    private final String name;
    private final List<FileSystemComponent> children = new ArrayList<>();

    public Folder(String name) { this.name = name; }

    public void add(FileSystemComponent c) {
        children.add(c);
    }

    public void display() {
        System.out.println("Folder: " + name);
        children.forEach(FileSystemComponent::display);
    }
}

Java Version Tracker

📌 What's New in Java?

  • Java 8
    • Map.computeIfAbsent(), List.forEach(), and lambdas made pattern use easier
  • Java 9
    • Immutable Map.of() and List.of() simplified pattern bootstrapping
  • Java 10+
    • var for cleaner factory and strategy code
  • Java 21
    • Virtual threads allow better async observers and event processors

Performance Considerations

Pattern Common Collection Access Time Thread-Safe Alternatives
Factory Map<String, Supplier> O(1) ConcurrentHashMap
Strategy Map<String, Strategy> O(1) EnumMap, ConcurrentMap
Observer List<Observer> O(n) CopyOnWriteArrayList
Composite List<Component> O(n) Usually not concurrent

Best Practices

  • Use Map<String, T> for fast dispatch (Factory, Strategy)
  • Favor List<T> for ordered composition (Composite, Observer)
  • Always return unmodifiable collections in public APIs
  • Use lazy loading with computeIfAbsent() where needed
  • Avoid modifying collections directly during iteration

Anti-Patterns

  • Using raw types (Map instead of Map<String, T>)
  • Not validating collection keys in factories
  • Forgetting to handle null values in strategy maps
  • Returning internal modifiable collections from services

Refactoring Legacy Code

  • Replace if-else or switch with Map<String, Function> for factories
  • Convert listener lists to CopyOnWriteArrayList for thread safety
  • Move from nested inheritance to composite structure with List

Real-World Use Cases

  • Spring ApplicationContext: BeanFactory registry using Map<String, Object>
  • Command Dispatchers: Map<CommandType, CommandHandler> for REST controllers
  • Metrics Collection: Observer pattern with listener list
  • Middleware Chains: Strategy + Decorator pattern via ordered List

Conclusion and Key Takeaways

  • Java Collections are the backbone of many design patterns
  • Map is powerful for lookup-based dispatching
  • List enables composite hierarchies, observers, and decorators
  • Knowing how to leverage collections inside patterns reduces complexity and increases flexibility

FAQ – Java Collections in Design Patterns

  1. Which design patterns use Map most effectively?
    Factory and Strategy — for quick object lookup and behavior switching.

  2. Why use List in Observer or Composite patterns?
    It maintains order and supports hierarchical relationships.

  3. How do I ensure thread safety in these patterns?
    Use concurrent collections like ConcurrentHashMap, CopyOnWriteArrayList.

  4. Can I use EnumMap for strategies?
    Yes — if your keys are enums, it's faster and more memory-efficient.

  5. Should collections be modifiable in design pattern objects?
    No — protect internal collections with unmodifiable wrappers.

  6. How do I replace switch-case with a collection?
    Use Map<String, Runnable> or Map<Enum, Command>.

  7. Is there a performance hit using Map for factories?
    Negligible — HashMap offers constant-time lookup.

  8. Are these collections serializable?
    Yes — but test custom strategies or lambdas separately.

  9. Can I inject these maps via Spring or DI?
    Absolutely — use @Bean methods or component scanning.

  10. What if I need to remove listeners dynamically?
    Use remove() on your List<Observer> implementation safely.