Skip to content
intermediatePhase 49 · Low-Level Design

Polymorphism

Enable flexible behavior through method overriding and interfaces.

30m
0 problems
Topic Progress0%

Compile-Time Polymorphism

Compile-time polymorphism (static binding) resolves method calls at compile time. The compiler determines which method to call based on the method signature.

Method Overloading

public class MathUtils {
    // Same name, different parameters
    public int add(int a, int b) {
        return a + b;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
    
    public String add(String a, String b) {
        return a + b;
    }
}

// Compiler resolves at compile time
MathUtils math = new MathUtils();
math.add(1, 2);        // Calls int version
math.add(1.5, 2.5);    // Calls double version
math.add(1, 2, 3);     // Calls 3-param version
math.add("Hi", " There"); // Calls String version

Constructor Overloading

public class User {
    private String name;
    private String email;
    private int age;
    
    public User() {
        this.name = "Anonymous";
    }
    
    public User(String name) {
        this.name = name;
    }
    
    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }
    
    public User(String name, String email, int age) {
        this.name = name;
        this.email = email;
        this.age = age;
    }
}

Operator Overloading (Some Languages)

# Python supports operator overloading
class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    
    def __add__(self, other):  # Overload + operator
        return Vector(self.x + other.x, self.y + other.y)
    
    def __repr__(self):  # Overload string representation
        return f"Vector({self.x}, {self.y})"

Benefits of Compile-Time Polymorphism

  1. Readability: Same operation name for different input types
  2. Convenience: Multiple ways to call the same function
  3. Performance: No runtime overhead (resolved at compile time)
  4. Type Safety: Compiler ensures correct parameter types

Runtime Polymorphism

Runtime polymorphism (dynamic binding) resolves method calls at runtime based on the actual object type, not the reference type.

Method Overriding

public interface PaymentProcessor {
    PaymentResult processPayment(Money amount);
}

public class CreditCardProcessor implements PaymentProcessor {
    @Override
    public PaymentResult processPayment(Money amount) {
        // Credit card logic
        return new PaymentResult(true, "CC-" + UUID.randomUUID());
    }
}

public class PayPalProcessor implements PaymentProcessor {
    @Override
    public PaymentResult processPayment(Money amount) {
        // PayPal logic
        return new PaymentResult(true, "PP-" + UUID.randomUUID());
    }
}

// Runtime polymorphism
PaymentProcessor processor = getProcessor(); // Could be either
processor.processPayment(amount); // Resolved at runtime

Polymorphic Collections

List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle(5));
shapes.add(new Rectangle(4, 6));
shapes.add(new Triangle(3, 4, 5));

// Same method call, different behavior at runtime
for (Shape shape : shapes) {
    System.out.println("Area: " + shape.getArea()); // Polymorphic!
    System.out.println("Perimeter: " + shape.getPerimeter());
}

Dynamic Method Dispatch

Shape shape = new Circle(5);

shape.getArea()  // Runtime:
│
├── Reference type: Shape (compile-time check)
├── Object type: Circle (runtime type)
└── Method called: Circle.getArea() (dynamic dispatch)

Use Cases in System Design

1. Plugin Architecture:

public interface Plugin {
    void execute(Request request);
}

// Plugins loaded at runtime
List<Plugin> plugins = loadPlugins();
for (Plugin plugin : plugins) {
    plugin.execute(request); // Each plugin has different behavior
}

2. Strategy Pattern:

public interface SortStrategy {
    void sort(int[] array);
}

// Different sorting strategies
SortStrategy strategy = getUserChoice();
strategy.sort(array); // Runtime determines algorithm

3. Factory Pattern:

public class NotificationFactory {
    public static Notification create(NotificationType type) {
        return switch (type) {
            case EMAIL -> new EmailNotification();
            case SMS -> new SMSNotification();
            case PUSH -> new PushNotification();
        };
    }
}

Notification notification = NotificationFactory.create(type);
notification.send(message); // Polymorphic behavior

Use Cases

Polymorphism is everywhere in system design. Recognizing when to apply it is a key skill.

Polymorphism in Real Systems

┌─────────────────────────────────────────────────────┐
│                 Notification System                  │
├─────────────────────────────────────────────────────┤
│                                                     │
│  Notification (interface)                           │
│  ├── EmailNotification                             │
│  ├── SMSNotification                               │
│  ├── PushNotification                              │
│  └── SlackNotification                             │
│                                                     │
│  User preferences determine which to use            │
│  Same API: notification.send(message)               │
└─────────────────────────────────────────────────────┘

Pattern Applications

Pattern Polymorphism Type Use Case
Strategy Runtime Algorithm selection
Observer Runtime Event handling
Factory Runtime Object creation
Template Method Runtime Algorithm skeleton
Decorator Runtime Adding behavior
Adapter Runtime Interface conversion

Designing for Polymorphism

1. Program to Interface:

// Good: Depends on interface
List<PaymentProcessor> processors = List.of(
    new CreditCardProcessor(),
    new PayPalProcessor()
);

// Bad: Depends on concrete class
List<CreditCardProcessor> processors = ...;

2. Open-Closed Principle:

// Open for extension: add new DiscountStrategy implementations
// Closed for modification: no changes to existing code
public interface DiscountStrategy {
    Money calculateDiscount(Order order);
}

public class PercentageDiscount implements DiscountStrategy { }
public class FixedAmountDiscount implements DiscountStrategy { }
public class BuyOneGetOneFree implements DiscountStrategy { }

3. Dependency Injection:

public class OrderService {
    private final PaymentProcessor processor;
    
    // Injected: runtime polymorphism
    public OrderService(PaymentProcessor processor) {
        this.processor = processor;
    }
    
    public Order checkout(Cart cart) {
        return processor.processPayment(cart.getTotal());
    }
}

When to Use Polymorphism

Situation Approach
Multiple implementations of same behavior Runtime polymorphism
Algorithm selection at runtime Strategy pattern
Same operation, different input types Compile-time (overloading)
Plugin/extension architecture Runtime polymorphism
Framework designed for extension Runtime polymorphism

Common Anti-Patterns

  1. Type checking with instanceof: Replace with polymorphism
  2. Switch on type: Use strategy/factory instead
  3. Downcasting: Indicates poor hierarchy design
  4. Leaking concrete types: Always return interfaces

Practice Problems

0/3solved
Design Polymorphism System

Design a scalable Polymorphism system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
Polymorphism Scaling

How would you scale Polymorphism to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
Polymorphism Failure Modes

Analyze potential failure modes for Polymorphism and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. What is the key difference between compile-time and runtime polymorphism?

Question 1 options

2. Which is an example of runtime polymorphism?

Question 2 options

3. What pattern uses runtime polymorphism for algorithm selection?

Question 3 options

4. Why should you program to interfaces in polymorphic designs?

Question 4 options

5. What does 'dynamic method dispatch' mean?

Question 5 options

Flashcards

Question

What is compile-time polymorphism?

Answer

Method overloading resolved at compile time. Same method name with different parameter lists. The compiler determines which method to call based on the method signature.

Question

What is runtime polymorphism?

Answer

Method overriding where the JVM determines which method to call at runtime based on the actual object type. Enables substituting different implementations through a common interface.

Question

What is dynamic method dispatch?

Answer

The runtime mechanism where the JVM selects the appropriate overridden method based on the actual object type, not the reference type. Key to runtime polymorphism.

Question

Name 3 design patterns that use runtime polymorphism.

Answer

Strategy (algorithm selection), Observer (event handling), Factory (object creation), Decorator (adding behavior), Adapter (interface conversion).

Question

Why program to interfaces in polymorphic designs?

Answer

Enables swapping implementations without changing consumer code. Provides loose coupling and flexibility. Code depends on contracts, not implementations.

Revision Notes

Key Takeaways

  • 1.Compile-time polymorphism uses method overloading; runtime uses method overriding
  • 2.Runtime polymorphism enables substituting implementations through common interfaces
  • 3.Design patterns like Strategy, Observer, and Factory leverage runtime polymorphism
  • 4.Always program to interfaces for maximum flexibility
  • 5.Dynamic dispatch determines the correct method at runtime based on object type

Interview Tips

  • Explain both types of polymorphism when asked about OOP concepts
  • Show how polymorphism enables the Strategy pattern in your design
  • Discuss dynamic dispatch when explaining why overridden methods are called
  • Mention programming to interfaces as a key polymorphism application

Cheat Sheet

Polymorphism - Cheat Sheet

Compile-Time (Static):

  • Method overloading
  • Resolved at compile time
  • Same name, different parameters
  • No runtime overhead

Runtime (Dynamic):

  • Method overriding
  • Resolved at runtime
  • Same signature, different implementations
  • Based on actual object type

Use Cases:

Pattern Type
Strategy Runtime
Observer Runtime
Factory Runtime
Decorator Runtime

Design for Polymorphism:

  1. Program to interface
  2. Follow Open-Closed Principle
  3. Use dependency injection
  4. Return interfaces, not concrete types

Anti-Patterns:

  • Switch on type → Use strategy
  • instanceof checks → Use polymorphism
  • Downcasting → Poor hierarchy design