Menu
CoddyTech

SOLID Principles and Design Patterns Interview Questions

The five SOLID principles, composition over inheritance, and the common patterns: singleton, factory, strategy, observer, builder.

14 questions
  1. What are the SOLID principles?

    Five design rules for classes that are easy to change:

    1. Single responsibility: one reason to change.
    2. Open/closed: add behavior with new classes, not edits.
    3. Liskov substitution: a subclass works anywhere its parent does.
    4. Interface segregation: no class implements methods it does not need.
    5. Dependency inversion: depend on abstractions, not concrete classes.
  2. Explain the single responsibility principle with an example.

    A class should have one reason to change. If tax rules, PDF layout and the database schema can each force edits to Invoice, it has three responsibilities:

    // Before: three reasons to change
    class Invoice {
        double total() { ... }          // business rules
        byte[] toPdf() { ... }          // presentation
        void saveToDb() { ... }         // persistence
    }
    
    // After: one each
    class Invoice { double total() { ... } }
    class InvoicePdfRenderer { byte[] render(Invoice inv) { ... } }
    class InvoiceRepository { void save(Invoice inv) { ... } }
    

    Now a layout change cannot break the tax calculation. Split when concerns change for different reasons, not into one-method classes.

  3. What is the open/closed principle?

    Classes should be open for extension and closed for modification: you add behavior by writing a new class, not by editing one that works.

    Java
    public class Main {
        public static void main(String[] args) {
            Checkout checkout = new Checkout();
            System.out.println(checkout.complete(new Upi(), 499));
            System.out.println(checkout.complete(new Card(), 1299));
        }
    }
    
    interface PaymentMethod { String pay(int amount); }
    
    class Upi implements PaymentMethod {
        public String pay(int amount) { return "UPI paid " + amount; }
    }
    
    class Card implements PaymentMethod {
        public String pay(int amount) { return "Card paid " + amount; }
    }
    
    class Checkout {
        String complete(PaymentMethod method, int amount) { return method.pay(amount); }
    }

    It prints UPI paid 499 and Card paid 1299. A wallet is one new class, and Checkout stays untouched. The violation is an if (type.equals("upi")) chain inside Checkout.

  4. What does resize(new Square()) print in Java, and which SOLID rule breaks?

    Java
    public class Main {
        static void resize(Rectangle r) {
            r.setWidth(5);
            r.setHeight(4);
            System.out.println(r.area());
        }
    
        public static void main(String[] args) {
            resize(new Square());
        }
    }
    
    class Rectangle {
        protected int width, height;
    
        void setWidth(int w) { width = w; }
    
        void setHeight(int h) { height = h; }
    
        int area() { return width * height; }
    }
    
    class Square extends Rectangle {
        @Override
        void setWidth(int w) { width = w; height = w; }
    
        @Override
        void setHeight(int h) { width = h; height = h; }
    }

    Square keeps itself square by setting both sides in each setter, so setHeight(4) also makes the width 4: the area is 16, not the 20 resize expects. That breaks the Liskov substitution principle: a subtype must honor its parent's behavior, not only its signatures. Fix it with immutable shapes, or a shared Shape interface.

  5. What is the interface segregation principle?

    No class should be forced to implement methods it does not use, so split a fat interface into small, focused ones:

    // Fat: a robot must pretend to eat
    interface Worker { void work(); void eat(); }
    
    // Segregated
    interface Workable { void work(); }
    interface Eater { void eat(); }
    
    class Human implements Workable, Eater { ... }
    class Robot implements Workable { ... }
    

    The symptom of a violation is an implementation that throws UnsupportedOperationException or leaves a method empty.

  6. What is the difference between dependency inversion and dependency injection?

    Dependency inversion is the principle: high-level code depends on an abstraction, not a concrete class. Dependency injection is one way to get there: the object receives its dependencies, usually through the constructor, instead of creating them with new.

    Java
    import java.util.ArrayList;
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            FakeNotifier fake = new FakeNotifier();
            new OrderService(fake).place("asha");
            System.out.println(fake.sent);
        }
    }
    
    interface Notifier { void send(String to, String message); }
    
    class FakeNotifier implements Notifier {
        final List<String> sent = new ArrayList<>();
    
        public void send(String to, String message) { sent.add(to + ": " + message); }
    }
    
    class OrderService {
        private final Notifier notifier;
    
        OrderService(Notifier notifier) { this.notifier = notifier; }
    
        void place(String user) { notifier.send(user, "order placed"); }
    }

    It prints [asha: order placed]; a test passes a fake sender.

  7. What is composition, and why prefer composition over inheritance?

    Composition builds a class from objects it holds (has-a) instead of extending one (is-a). Prefer it because inheritance exposes every public method of the parent and ties you to its implementation. java.util.Stack extends Vector, so it accepts list operations that break the stack:

    Java
    import java.util.Stack;
    
    public class Main {
        public static void main(String[] args) {
            Stack<Integer> stack = new Stack<>();
            stack.push(1);
            stack.push(2);
            stack.add(0, 99);
            System.out.println(stack + " pop=" + stack.pop());
        }
    }

    It prints [99, 1, 2] pop=2. A stack that holds an ArrayDeque cannot be misused that way.

    Practice it: Implement Queue Using Stacks

  8. What is the difference between association, aggregation and composition?

    They differ in ownership and lifetime. Association: objects use each other and live independently (a teacher and a student). Aggregation: has-a, and the parts can outlive the whole (a team and its players). Composition: the whole owns its parts, and they die with it (a house and its rooms).

  9. What are coupling and cohesion?

    Cohesion is how closely the parts of one class belong together; coupling is how much a class depends on another's internals. Aim for high cohesion and low coupling. OrderService creating new MySqlOrderDao() is tightly coupled; taking an OrderRepository interface in its constructor is loose.

  10. How do you implement a thread-safe singleton in Java?

    A singleton guarantees one instance with a global access point. The thread-safe versions are the enum singleton and the holder idiom; both rely on the JVM running class initialization once, under a lock.

    Java
    public class Main {
        public static void main(String[] args) {
            System.out.println("start");
            Config a = Config.get();
            Config b = Config.get();
            System.out.println(a == b);
        }
    }
    
    class Config {
        private Config() { System.out.println("loading config"); }
    
        private static class Holder {
            static final Config INSTANCE = new Config();
        }
    
        static Config get() { return Holder.INSTANCE; }
    }

    It prints start, loading config, true: created lazily, once. Double-checked locking works only with a volatile field.

  11. What is the factory design pattern?

    A factory is a method or class that creates objects, so callers ask for a notification for a channel and never name the concrete class. It keeps new ConcreteClass() in one place.

    Java
    import java.util.List;
    
    public class Main {
        public static void main(String[] args) {
            for (String channel : List.of("email", "sms")) {
                System.out.println(NotificationFactory.create(channel).send("OTP 4821"));
            }
        }
    }
    
    interface Notification { String send(String message); }
    
    class EmailNotification implements Notification {
        public String send(String message) { return "email: " + message; }
    }
    
    class SmsNotification implements Notification {
        public String send(String message) { return "sms: " + message; }
    }
    
    class NotificationFactory {
        static Notification create(String channel) {
            return switch (channel) {
                case "email" -> new EmailNotification();
                case "sms" -> new SmsNotification();
                default -> throw new IllegalArgumentException("unknown channel " + channel);
            };
        }
    }

    It prints email: OTP 4821 and sms: OTP 4821. List.of and Integer.valueOf are static factories in the JDK.

  12. What is the strategy pattern?

    The strategy pattern puts interchangeable algorithms behind one interface, so the caller picks one at runtime instead of branching with if/else. With lambdas a strategy is one line:

    Java
    import java.util.LinkedHashMap;
    import java.util.Map;
    import java.util.function.DoubleUnaryOperator;
    
    public class Main {
        public static void main(String[] args) {
            Map<String, DoubleUnaryOperator> shipping = new LinkedHashMap<>();
            shipping.put("standard", kg -> 40 + 10 * kg);
            shipping.put("express", kg -> 100 + 25 * kg);
            shipping.put("pickup", kg -> 0);
            for (var entry : shipping.entrySet()) {
                System.out.println(entry.getKey() + " " + entry.getValue().applyAsDouble(2));
            }
        }
    }

    It prints standard 60.0, express 150.0 and pickup 0.0. Comparator is the strategy everyone uses: list.sort(byName) versus list.sort(byAge).

  13. What is the observer pattern?

    A subject keeps a list of listeners and notifies all of them when something happens, without knowing what they do.

    Java
    import java.util.ArrayList;
    import java.util.List;
    import java.util.function.Consumer;
    
    public class Main {
        public static void main(String[] args) {
            OrderEvents events = new OrderEvents();
            events.subscribe(id -> System.out.println("email sent for " + id));
            events.subscribe(id -> System.out.println("stock updated for " + id));
            events.publish("A17");
        }
    }
    
    class OrderEvents {
        private final List<Consumer<String>> listeners = new ArrayList<>();
    
        void subscribe(Consumer<String> listener) { listeners.add(listener); }
    
        void publish(String orderId) {
            for (Consumer<String> listener : listeners) listener.accept(orderId);
        }
    }

    It prints email sent for A17 then stock updated for A17. Pitfall: listeners that are never removed leak memory.

  14. What is the builder pattern, and when do you use it?

    A builder sets an object's values through named calls and creates it in one build(). Use it when a class has many optional parameters, instead of telescoping constructors such as new User("Asha", null, null, 0, true).

    Java
    public class Main {
        public static void main(String[] args) {
            UserProfile p = new UserProfile.Builder("Asha")
                    .email("asha@example.com")
                    .city("Pune")
                    .build();
            System.out.println(p);
        }
    }
    
    class UserProfile {
        private final String name;
        private final String email;
        private final String city;
    
        private UserProfile(Builder b) {
            name = b.name;
            email = b.email;
            city = b.city;
        }
    
        @Override
        public String toString() { return name + " <" + email + "> " + city; }
    
        static class Builder {
            private final String name;
            private String email = "none";
            private String city = "unknown";
    
            Builder(String name) { this.name = name; }
    
            Builder email(String email) { this.email = email; return this; }
    
            Builder city(String city) { this.city = city; return this; }
    
            UserProfile build() { return new UserProfile(this); }
        }
    }

    It prints Asha <asha@example.com> Pune. build() is the one place to validate, and the result can be immutable.

Coddy programming languages illustration

Learn to code with Coddy

Get started