AlgoDaily

Design Patterns and Best Practices

Read at your own pace. Code execution, quizzes, progress tracking, and the AI tutor are available in the interactive view with JavaScript enabled.

Patterns As Shortcuts

Why this lesson matters

Now that we've covered High-Level System Design, Low-Level Design Principles, Microservices Architecture, Data Design, and Cloud-based Architecture, you can already draw a system and break it into services, classes, data models, and deployments.

Design patterns help with the next question:

When I see a familiar design problem, what proven shape should my code take?

Think of patterns as named solutions to recurring forces:

  • changing business rules
  • object creation complexity
  • integration with incompatible APIs
  • event notifications
  • repeated workflow steps

In the upcoming e-commerce project, these patterns will help us keep the codebase understandable instead of turning every service into a pile of if/else statements.

Patterns As Shortcuts

Pattern Selection Map

Do not memorize patterns—recognize smells

A good pattern usually starts with a pain signal in your code.

Pain signal Useful pattern Layman analogy Too many if/else branches for different rules Strategy Pick a payment card at checkout Object creation has many variants Factory Restaurant kitchen preparing the right dish External API shape does not match yours Adapter Travel plug converter Many parts need to react to one change Observer Subscribers receiving a notification Same workflow, different small steps Template Method Same recipe, different toppings

Best practice: name the problem first, then choose the pattern. If there is no real pain, adding a pattern can make the code harder, not better.

Pattern Selection Map

Strategy In Java

Strategy: swap behavior without rewriting the caller

From Low-Level Design Principles, you already know we should depend on interfaces when behavior varies. The Strategy pattern gives that idea a name.

Use it when:

  • the algorithm changes by customer type, country, tenant, plan, or feature flag
  • the caller should not know the implementation details
  • you want to test each rule independently

Example: an order service calculates final price, but discount rules change often.

```java
public class Main {
    interface DiscountPolicy {
        double apply(double originalPrice);
    }

    static class NoDiscount implements DiscountPolicy {
        public double apply(double originalPrice) {
            return originalPrice;
        }
    }

    static class PremiumCustomerDiscount implements DiscountPolicy {
        public double apply(double originalPrice) {
            return originalPrice * 0.90;
        }
    }

    static class FestivalDiscount implements DiscountPolicy {
        public double apply(double originalPrice) {
            return originalPrice * 0.75;
        }
    }

    static class PriceCalculator {
        private final DiscountPolicy discountPolicy;

        PriceCalculator(DiscountPolicy discountPolicy) {
            this.discountPolicy = discountPolicy;
        }

        double finalPrice(double originalPrice) {
            return discountPolicy.apply(originalPrice);
        }
    }

    public static void main(String[] args) {
        System.out.println("Regular: " + new PriceCalculator(new NoDiscount()).finalPrice(100));
        System.out.println("Premium: " + new PriceCalculator(new PremiumCustomerDiscount()).finalPrice(100));
        System.out.println("Festival: " + new PriceCalculator(new FestivalDiscount()).finalPrice(100));
    }
}
```

Strategy Use Case

You are building a Spring Boot checkout service. Different customers may use different pricing rules: student discount, premium discount, seasonal discount, or no discount.

Which design choice best matches the Strategy pattern?

Factory And Adapter

Factory: centralize object creation

A Factory is useful when the caller knows what kind of thing it wants, but should not know the exact class construction details.

In Spring Boot, dependency injection often handles object creation for you. But factories are still useful for runtime decisions, such as choosing an email, SMS, or push notification sender.

Adapter: make incompatible shapes work together

An Adapter wraps one interface and presents another. This is common in microservices when integrating with payment gateways, shipping APIs, or legacy systems.

Rule of thumb:

  • Factory answers: Which object should I create?
  • Adapter answers: How do I make this external thing look like my interface?
```java
public class Main {
    interface NotificationSender {
        void send(String to, String message);
    }

    static class EmailSender implements NotificationSender {
        public void send(String to, String message) {
            System.out.println("EMAIL to " + to + ": " + message);
        }
    }

    static class SmsSender implements NotificationSender {
        public void send(String to, String message) {
            System.out.println("SMS to " + to + ": " + message);
        }
    }

    static class NotificationFactory {
        static NotificationSender create(String channel) {
            if ("email".equalsIgnoreCase(channel)) {
                return new EmailSender();
            }
            if ("sms".equalsIgnoreCase(channel)) {
                return new SmsSender();
            }
            throw new IllegalArgumentException("Unknown channel: " + channel);
        }
    }

    public static void main(String[] args) {
        NotificationSender sender = NotificationFactory.create("sms");
        sender.send("+1-555-0100", "Your order has shipped");
    }
}
```

Pattern Restraint

True or false: A design pattern is always an improvement, even if the code is currently simple and unlikely to change.

Observer Mental Model

Observer: notify many listeners without tight coupling

The Observer pattern is useful when one event should trigger multiple reactions.

Example: an order is placed.

Possible listeners:

  • inventory reserves stock
  • payment starts authorization
  • notification sends confirmation
  • analytics records conversion

In a monolith, this may be an in-process event. In microservices, as we discussed in Microservices Architecture, the same idea often appears as events published to Kafka, RabbitMQ, SNS/SQS, or another broker.

Important boundary: Observer describes the relationship. The infrastructure choice belongs to your architecture and cloud design decisions.

Observer Mental Model

Template Method Workflow

Template Method: same skeleton, customizable steps

Use Template Method when multiple flows share the same high-level process but differ in a few steps.

Example: order processing might always do this:

  1. validate order
  2. collect payment
  3. fulfill order
  4. notify customer

But fulfillment differs for physical and digital products.

This pattern keeps the workflow consistent while allowing safe extension points.

```java
public class Main {
    static abstract class OrderProcessor {
        public final void process(String orderId) {
            validate(orderId);
            collectPayment(orderId);
            fulfill(orderId);
            notifyCustomer(orderId);
        }

        private void validate(String orderId) {
            System.out.println("Validated order " + orderId);
        }

        private void collectPayment(String orderId) {
            System.out.println("Collected payment for " + orderId);
        }

        protected abstract void fulfill(String orderId);

        private void notifyCustomer(String orderId) {
            System.out.println("Notified customer for " + orderId);
        }
    }

    static class PhysicalOrderProcessor extends OrderProcessor {
        protected void fulfill(String orderId) {
            System.out.println("Packed and shipped physical order " + orderId);
        }
    }

    static class DigitalOrderProcessor extends OrderProcessor {
        protected void fulfill(String orderId) {
            System.out.println("Generated download link for digital order " + orderId);
        }
    }

    public static void main(String[] args) {
        new PhysicalOrderProcessor().process("ORD-101");
        System.out.println("---");
        new DigitalOrderProcessor().process("ORD-202");
    }
}
```

Name The Pattern

In low-level design, the pattern that lets you swap algorithms behind the same interface is called the ____ pattern.

Best Practices Checklist

Practical best practices

Patterns matter only when they improve the system's ability to change safely.

Use this checklist before adding a pattern:

  • Start simple. Do not abstract before you understand the variation.
  • Depend on interfaces for unstable behavior. This builds on our LLD lesson.
  • Keep business rules out of controllers. Controllers should coordinate, not own domain logic.
  • Use clear names. DiscountPolicy is better than IDoStuff.
  • Prefer composition over inheritance unless a fixed workflow truly fits Template Method.
  • Test each behavior separately. A Strategy should be easy to unit test.
  • Do not leak infrastructure into domain logic. Payment rules should not know Kafka or Kubernetes details.
  • Document tradeoffs. Patterns add indirection, so explain why it exists.

Next, we will apply these ideas in a real e-commerce system design without turning the lesson into a pattern catalog.

Best Practices Checklist

Open interactive view →