Chapter 5: Low-Level Design (LLD)

5.1 Object-Oriented Design

## OOP Concepts

Object-oriented programming organizes code around objects that combine data and behavior.

Instead of writing procedures that operate on separate data structures, you create objects that own their data and expose controlled operations on it.

OOP is not just a programming paradigm. It is a way of thinking about how to model real-world problems in code.

**Encapsulation, Abstraction, Inheritance, Polymorphism**

These four concepts form the foundation of OOP.

Every design pattern, every principle, and every architectural decision in this chapter builds on them.

Encapsulation means bundling data and the methods that operate on that data inside a single unit (a class), and restricting direct access to the internal state. A `BankAccount` class stores the balance privately. External code cannot set the balance directly. It must call `deposit()` or `withdraw()`, which enforce business rules (you cannot withdraw more than the balance). Encapsulation protects the object's internal consistency.

Abstraction means exposing only what matters and hiding the complexity behind it. When you call `emailService.send(recipient, subject, body)`, you do not care whether the service uses SMTP, a third-party API, or a message queue internally. The abstraction lets you use the service without understanding its internals. Abstraction reduces cognitive load: each piece of code only needs to understand the interface of what it depends on, not the implementation.

Inheritance means a class can derive from another class, inheriting its properties and methods. A `SavingsAccount` class might inherit from `BankAccount`, gaining balance tracking and withdraw functionality, then add interest calculation. Inheritance creates a hierarchy and enables code reuse. But overusing it creates rigid, deeply nested hierarchies that are hard to modify (more on this below).

Polymorphism means objects of different classes can be treated through the same interface. If `CreditCardPayment`, `BankTransferPayment`, and `WalletPayment` all implement a `PaymentMethod` interface with a `charge(amount)` method, the calling code does not care which specific type it is dealing with. It calls `charge()` and the right implementation executes. Polymorphism is what makes extensible, flexible systems possible.

**Composition Over Inheritance**

Inheritance creates an "is-a" relationship.

A `Dog` is an `Animal`.

A `SavingsAccount` is a `BankAccount`.

Composition creates a "has-a" relationship.

A `Car` has an `Engine`. A `NotificationService` has an `EmailSender` and an `SMSSender`.

Inheritance becomes problematic when hierarchies get deep.

If `Vehicle` extends `Transport`, which extends `Movable`, which extends `Entity`, changing anything in `Entity` can ripple through every class in the chain.

Adding a new capability (like flying) might require restructuring the entire hierarchy if it does not fit neatly into the existing tree.

Composition avoids these problems by assembling objects from smaller, focused components.

A `Duck` class does not need to inherit from `FlyingAnimal` and `SwimmingAnimal` (which many languages do not support simultaneously).

Instead, a `Duck` has a `FlyBehavior` and a `SwimBehavior`. You can swap behaviors at runtime. You can add new behaviors without touching the existing class hierarchy.

The practical guideline: use inheritance for genuine "is-a" relationships where the child class truly is a specialization of the parent.

Use composition for everything else.

In most codebases, composition appears far more frequently than inheritance.

**Interfaces and Abstract Classes**

An interface defines a contract: a set of methods that any implementing class must provide. It specifies what an object can do without specifying how.

A `PaymentProcessor` interface might require `charge()`, `refund()`, and `getTransactionStatus()`.

Any class that implements this interface must provide all three methods.

An abstract class is a class that cannot be instantiated directly. It provides a partial implementation that subclasses complete.

An abstract `DatabaseConnector` might implement connection pooling and logging (shared behavior) while leaving the actual query execution abstract for subclasses like `PostgresConnector` and `MySQLConnector` to implement.

ConceptProvides Implementation?Multiple Inheritance?State (Fields)?
InterfaceNo (only the contract)Yes (a class can implement many)No (in most languages)
Abstract classPartialNo (single inheritance)Yes

Use interfaces when you want to define a capability that multiple unrelated classes can implement.

Use abstract classes when you want to share code between related classes that have a common base.

**Object-Oriented Analysis and Design (OOAD)**

OOAD is the process of analyzing a problem, identifying the objects in it, and designing the classes and their relationships before writing code.

The process follows three steps.

Analysis asks:

* What are the entities in this problem? * What data do they hold? * What actions do they perform? * What relationships connect them?

For a parking lot system, the entities include ParkingLot, ParkingFloor, ParkingSpot, Vehicle, Ticket, and PaymentTransaction.

Design defines the classes, their attributes, their methods, and the relationships (inheritance, composition, association) between them.

Implementation writes the actual code based on the design.

OOAD produces better designs than jumping straight into code because it forces you to think about structure before details.

In interviews, especially OOD interviews), the interviewer evaluates your analysis and design process, not your ability to write syntactically correct code.

Interview-Style Question

> Q: What is the difference between an interface and an abstract class? When would you use each?

> A: An interface defines a pure contract with no implementation. It says "any class that implements me must have these methods." A class can implement multiple interfaces. Use interfaces when unrelated classes share a common capability (like `Serializable` or `PaymentProcessor`). An abstract class provides a partial implementation with some methods defined and some left abstract. A class can extend only one abstract class. Use abstract classes when related classes share common behavior (like a `DatabaseConnector` base that handles connection pooling) but differ in specific details (like how queries are executed for different database engines). In practice, default to interfaces for contracts and abstract classes for shared base behavior.

**KEY TAKEAWAYS**

* Encapsulation protects internal state. Abstraction hides complexity. Inheritance enables specialization. Polymorphism enables treating different types through a common interface.

* Prefer composition over inheritance. Assemble objects from focused components rather than building deep class hierarchies. * Use interfaces for contracts across unrelated classes. Use abstract classes for shared behavior among related classes. * OOAD analyzes the problem domain to identify entities, relationships, and behaviors before writing code. It produces cleaner designs than coding first.

## Design Principles

Design principles are guidelines that help you write code that is easy to understand, modify, and extend. They are not rigid laws. They are judgments refined through decades of collective engineering experience.

Knowing them helps you recognize when a design is headed toward trouble and how to steer it back.

**SOLID Principles**

SOLID is an acronym for five principles that guide class design. They were formalized by Robert C. Martin and have become the standard vocabulary for discussing object-oriented design quality.

#### S: Single Responsibility Principle (SRP)

A class should have one reason to change.

If a `UserService` handles user registration, password resets, profile updates, and also sends notification emails, it has four reasons to change.

When the email provider changes, you modify the same class that handles registration.

Split it: `UserService` handles user operations, `NotificationService` handles sending emails. Each class has one job.

#### O: Open/Closed Principle (OCP)

A class should be open for extension but closed for modification.

If you need to add a new payment method, you should not modify the existing `PaymentProcessor` class (risking breaking existing payment methods).

Instead, create a new class (`CryptoPaymentProcessor`) that implements the `PaymentProcessor` interface.

The existing code stays untouched. New behavior is added through new code, not changed code.

#### L: Liskov Substitution Principle (LSP)

A subclass should be usable wherever its parent class is expected, without breaking the program.

If `Rectangle` has a `setWidth()` and `setHeight()` method, and `Square` extends `Rectangle`, calling `setWidth(5)` on a Square should not break assumptions.

But a Square has equal sides, so setting width without setting height violates the caller's expectations.

This signals that Square should not extend Rectangle. LSP catches inheritance relationships that look logical but are technically broken.

#### I: Interface Segregation Principle (ISP)

No class should be forced to implement methods it does not use.

If a `Worker` interface has `work()`, `eat()`, and `sleep()` methods, a `RobotWorker` class is forced to implement `eat()` and `sleep()` even though they are meaningless for a robot.

Split the interface: `Workable` with `work()`, `Eatable` with `eat()`, `Sleepable` with `sleep()`. Classes implement only the interfaces they need.

#### D: Dependency Inversion Principle (DIP)

High-level modules should not depend on low-level modules. Both should depend on abstractions.

A `ReportGenerator` should not depend directly on `MySQLDatabase`. It should depend on a `DataSource` interface. `MySQLDatabase` implements `DataSource`.

If you later switch to PostgreSQL, you write a `PostgresDatabase` that implements the same `DataSource` interface.

The `ReportGenerator` code does not change.

**DRY (Don't Repeat Yourself)**

Every piece of knowledge or logic should exist in exactly one place. If the same validation rule appears in three different classes, a change to the rule requires three modifications. Miss one, and you have an inconsistency.

DRY does not mean eliminating all code duplication mechanically.

Two pieces of code that look identical but represent different business rules should remain separate.

If the discount calculation for new customers and the discount calculation for bulk orders happen to use the same formula today, merging them into one function creates a coupling that will hurt when the business changes one rule but not the other.

DRY applies to knowledge and intent, not just syntax.

**KISS (Keep It Simple, Stupid)**

The simplest solution that meets the requirements is usually the best one.

A three-class hierarchy with abstract factories and strategy patterns to solve a problem that a single function could handle is over-engineering.

Complexity should be proportional to the problem's complexity.

KISS does not mean writing simplistic or naive code. It means not introducing complexity before it is needed.

A clean, straightforward implementation that someone new to the codebase can understand in five minutes is better than an architecturally "elegant" solution that requires a whiteboard session to explain.

**YAGNI (You Ain't Gonna Need It)**

Do not build features, abstractions, or infrastructure for requirements that do not exist yet.

Engineers love predicting future needs: "We might need to support multiple currencies someday, so let's build the entire multi-currency framework now."

Three years later, the system still only supports USD, and the multi-currency code is a maintenance burden nobody uses.

Build for the requirements you have today.

Design your code so that adding multi-currency support later is feasible (use good abstractions, follow OCP), but do not build the implementation until the need is real. This is the balance: design for extensibility, build for today.

**Separation of Concerns**

Each module or component should handle one well-defined concern.

The presentation layer handles rendering UI.

The business logic layer handles rules and computations.

The data access layer handles database communication. Mixing concerns (embedding SQL queries in UI code, putting rendering logic in database models) creates code that is harder to test, harder to modify, and harder to reuse.

Separation of concerns is the principle behind layered architecture, hexagonal architecture, and microservices (each service handles one business domain).

**Law of Demeter**

The Law of Demeter (also called the "principle of least knowledge") says that an object should only talk to its immediate friends. It should not reach through one object to access another.

Bad: `order.getCustomer().getAddress().getCity()`. This chain means the calling code needs to know about Customer, Address, and City.

If Address changes, code that has nothing to do with addresses breaks.

Good: `order.getShippingCity()`. The Order object handles the traversal internally.

The caller only needs to know about Order.

If the internal structure changes, only Order's implementation is updated.

The Law of Demeter reduces coupling. Each object knows less about the structure of other objects, making the system more resilient to change.

Interview-Style Question

> Q: Give an example of how the Single Responsibility Principle would change the design of a class that handles both order processing and email notifications.

> A: An `OrderProcessor` that creates orders, calculates totals, persists to the database, and sends confirmation emails has four responsibilities. Any change to the email template, email provider, database schema, or pricing logic requires modifying this one class. Split it into `OrderProcessor` (validates and creates orders), `PricingCalculator` (computes totals and discounts), `OrderRepository` (handles database persistence), and `NotificationService` (sends emails). Each class has a single reason to change. The `OrderProcessor` orchestrates the flow by calling the others. If you switch email providers, only `NotificationService` changes. If the pricing formula changes, only `PricingCalculator` changes.

**KEY TAKEAWAYS**

* SOLID principles (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) guide class design toward code that is easy to extend and maintain. * DRY eliminates duplicated knowledge, not just duplicated syntax. Two identical-looking code blocks with different business meanings should stay separate. * KISS says use the simplest solution that works. YAGNI says do not build for imagined future requirements. Both prevent over-engineering. * Separation of concerns keeps each module focused on one area. The Law of Demeter reduces coupling by limiting how deeply objects reach into each other's structure.

## Design Patterns

Design patterns are reusable solutions to common design problems. They are not code you copy-paste. They are templates that you adapt to your specific situation.

The 23 patterns formalized by the "Gang of Four" (GoF) in 1994 remain the vocabulary that engineers use to communicate design decisions.

This section covers the most frequently used patterns, organized into three categories.

Understanding these patterns is essential for both OOD interviews and for working on real codebases.

Grokking the System Design Interview covers design patterns in detail.

**Creational Patterns**

Creational patterns control how objects are created.

  1. 1.Singleton ensures a class has exactly one instance and provides a global point of access to it. A database connection pool, a configuration manager, or a logging service might be singletons. Use it when exactly one instance should coordinate actions across the system. Overuse it and you create hidden global state that makes testing difficult.
  2. 2.Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate. A `NotificationFactory` might return an `EmailNotification`, `SMSNotification`, or `PushNotification` depending on the user's preferences. The calling code works with the `Notification` interface without knowing the specific type.
  3. 3.Abstract Factory creates families of related objects without specifying their concrete classes. A `UIFactory` might produce `Button`, `TextField`, and `Dropdown` objects. A `MaterialUIFactory` creates Material-styled components. A `BootstrapUIFactory` creates Bootstrap-styled components. The application works with the factory interface and gets a consistent set of components without knowing which style is used.
  4. 4.Builder separates the construction of a complex object from its representation. Instead of a constructor with 15 parameters (`new Order(userId, items, shipping, billing, discount, coupon, notes, ...)`), a Builder lets you set properties step by step: `Order.builder().userId(42).items(items).shipping(express).build()`. Builders make complex object creation readable and prevent parameter-ordering mistakes.
  5. 5.Prototype creates new objects by cloning an existing instance rather than constructing from scratch. Useful when object creation is expensive (involving database calls or complex calculations) and you have a baseline object that can be cloned and modified.

**Structural Patterns**

Structural patterns organize how classes and objects are composed into larger structures.

  1. 1.Adapter converts one interface into another that the client expects. Your application needs a `PaymentGateway` interface with `charge()` and `refund()`. A third-party library has a `StripeClient` with `createCharge()` and `createRefund()`. An adapter wraps `StripeClient` and translates its methods to match `PaymentGateway`. The application never touches the Stripe-specific code directly.
  2. 2.Bridge separates an abstraction from its implementation so both can vary independently. A `Notification` abstraction (urgent, standard, low-priority) and a `Channel` implementation (email, SMS, push) can be combined freely. `UrgentNotification` over `SMSChannel`. `StandardNotification` over `EmailChannel`. Adding a new priority does not affect channels, and adding a new channel does not affect priorities.
  3. 3.Composite treats individual objects and groups of objects through the same interface. A file system where both `File` and `Directory` implement a `FileSystemComponent` interface. A `Directory` contains `FileSystemComponents`, which can be files or other directories. Calling `getSize()` on a directory recursively sums the sizes of all contained components.
  4. 4.Decorator adds behavior to an object dynamically without modifying its class. A `BasicLogger` writes plain text. A `TimestampDecorator` wraps the logger and adds timestamps to every message. A `JSONDecorator` wraps it further and formats output as JSON. You can stack decorators to compose behavior: `new JSONDecorator(new TimestampDecorator(new BasicLogger()))`.
  5. 5.Facade provides a simplified interface to a complex subsystem. A `VideoConverter` facade might hide the complexity of codec selection, frame rate adjustment, audio normalization, and file writing behind a single `convert(input, outputFormat)` method.
  1. 1.Flyweight shares common data across many objects to save memory. In a text editor, each character on screen could be an object. Instead of storing the font, size, and color on every character, the flyweight shares these attributes across all characters that use the same formatting. Only the position is unique per character.
  2. 2.Proxy provides a substitute or placeholder for another object. A `LazyLoadingProxy` for a large image does not load the image from disk until someone actually requests the pixel data. A `CachingProxy` caches the results of expensive method calls. An `AccessControlProxy` checks permissions before delegating to the real object.

**Behavioral Patterns**

Behavioral patterns manage communication and responsibility between objects.

  1. 1.Observer defines a one-to-many dependency. When one object changes state, all its dependents are notified automatically. An `OrderService` publishes state changes. `InventoryObserver`, `NotificationObserver`, and `AnalyticsObserver` each react independently. Adding a new observer does not modify the subject.
  2. 2.Strategy defines a family of algorithms, encapsulates each one, and makes them interchangeable. A `SortingService` might accept a `SortStrategy` (QuickSort, MergeSort, TimSort). The calling code picks the strategy based on data characteristics without changing the sorting service itself.
  3. 3.Command encapsulates a request as an object, allowing you to parameterize operations, queue them, log them, and support undo. Each user action in a text editor (type, delete, format) becomes a Command object with `execute()` and `undo()` methods. The undo stack is a list of Command objects.
  4. 4.State lets an object alter its behavior when its internal state changes. A `Order` might be in states: Created, Paid, Shipped, Delivered, Cancelled. Each state is a class that defines the valid operations. Calling `ship()` on a Paid order transitions to Shipped. Calling `ship()` on a Created order throws an error. The state pattern eliminates large if/else chains for state-dependent logic.
  1. 1.Template Method defines the skeleton of an algorithm in a base class, letting subclasses override specific steps without changing the overall structure. A `DataImporter` base class defines the flow: `readFile()`, `parseRows()`, `validateData()`, `saveToDatabase()`. Subclasses override `parseRows()` for CSV, JSON, or XML without duplicating the rest of the pipeline.
  2. 2.Iterator provides a way to access elements of a collection sequentially without exposing the collection's internal structure. Most modern languages include iterators in their standard library (for-each loops, generators, streams).
  3. 3.Mediator centralizes complex communication between objects. Instead of 10 UI components communicating directly with each other (90 potential connections), they all communicate through a mediator (10 connections). A chat room is a mediator: participants send messages to the room, and the room distributes them.
  4. 4.Chain of Responsibility passes a request along a chain of handlers. Each handler either processes the request or passes it to the next handler. Middleware in web frameworks (Express.js, Django, Spring) follows this pattern: authentication middleware, logging middleware, rate limiting middleware, and the actual handler form a chain.

Interview-Style Question

> Q: You are designing a notification system that sends messages via email, SMS, push, and in-app channels. Different users prefer different channels, and new channels may be added in the future. Which design pattern would you use?

> A: Strategy pattern. Define a `NotificationChannel` interface with a `send(user, message)` method. Implement `EmailChannel`, `SMSChannel`, `PushChannel`, and `InAppChannel`. The `NotificationService` receives a list of channels for each user (based on their preferences) and calls `send()` on each. Adding a new channel (like WhatsApp) means creating a new class that implements `NotificationChannel`. The `NotificationService` does not change. If you also need to combine channels with priorities or formatting (urgent messages get a different template), the Bridge pattern pairs well with Strategy to separate the priority dimension from the channel dimension.

**KEY TAKEAWAYS**

* Creational patterns (Singleton, Factory, Builder, Prototype) control object creation. Use Factory for flexible creation, Builder for complex objects, and Singleton sparingly.

* Structural patterns (Adapter, Decorator, Facade, Proxy, Composite) organize object composition. Adapter bridges incompatible interfaces. Decorator adds behavior dynamically. Facade simplifies complex subsystems. * Behavioral patterns (Observer, Strategy, Command, State, Chain of Responsibility) manage object interaction. Strategy swaps algorithms. Observer decouples event producers from consumers. Command enables undo and queuing. * Design patterns are vocabulary, not templates to apply everywhere. Use them when they solve a specific problem. Do not force a pattern where a simple function would suffice.

## UML Diagrams

UML (Unified Modeling Language) provides a standardized visual vocabulary for describing software designs.

You do not need to master every UML diagram type, but knowing the most commonly used ones helps you communicate designs clearly in interviews, design reviews, and documentation.

**Class Diagrams**

Class diagrams show the static structure of a system: classes, their attributes and methods, and the relationships between them.

A class is drawn as a rectangle divided into three sections: the class name at the top, attributes in the middle, and methods at the bottom.

Visibility markers indicate access: `+` for public, `-` for private, `#` for protected.

Relationships between classes include association (a simple line, one class uses another), aggregation (a diamond-ended line, a class contains another but the contained object can exist independently), composition (a filled diamond, a class owns another and the contained object's lifecycle is tied to the container), and inheritance (an arrow with a hollow triangle pointing to the parent class).

Class diagrams are the most common UML diagram in OOD interviews. You sketch them on the whiteboard to show the interviewer your class structure, relationships, and key methods.

**Sequence Diagrams**

Sequence diagrams show how objects interact over time for a specific scenario.

Participants are shown as vertical lifelines.

Messages flow horizontally between lifelines, ordered from top to bottom chronologically.

A sequence diagram for "user places an order" might show: the User sends `placeOrder()` to the OrderController.

The OrderController sends `validatePayment()` to the PaymentService.

The PaymentService sends `charge()` to the PaymentGateway.

The PaymentGateway returns success.

The OrderController sends `createOrder()` to the OrderRepository.

The OrderRepository returns the order.

The OrderController returns the confirmation to the User.

Sequence diagrams reveal the communication flow and help identify overly chatty interactions, missing error handling, and dependency chains.

**Activity Diagrams**

Activity diagrams model workflows and business processes as a sequence of activities with decision points, parallel paths, and merge points. They look similar to flowcharts but with standardized UML notation.

An activity diagram for order processing might show: receive order → validate payment (decision: approved or declined) → if approved: reserve inventory → generate invoice → ship order → notify customer. If declined: notify customer of failure.

Parallel paths show activities that happen simultaneously (like generating the invoice and notifying the warehouse).

**Use Case Diagrams**

Use case diagrams show what a system does from the user's perspective.

Actors (stick figures representing users or external systems) connect to use cases (ovals representing system capabilities).

A use case diagram for an e-commerce system might show a Customer actor connected to "Browse Products," "Place Order," "Track Order," and "Return Item" use cases.

An Admin actor connects to "Manage Inventory," "View Reports," and "Manage Users."

Use case diagrams are useful early in design to define scope and identify the main interactions. They do not show how things work internally.

**State Machine Diagrams**

State machine diagrams show the states an object can be in and the transitions between them triggered by events.

An order might transition through states: Created → Paid → Shipped → Delivered (or Created → Cancelled, Shipped → Returned).

Each transition is labeled with the event that triggers it. State machine diagrams are particularly useful for designing workflows, protocols, and any object with complex lifecycle rules.

**Component and Deployment Diagrams**

Component diagrams show the high-level components of a system and their dependencies.

A component might be a microservice, a library, a database, or an external system.

Arrows show dependencies between components. These diagrams are useful for communicating architecture to stakeholders who do not need class-level detail.

Deployment diagrams show the physical or cloud infrastructure and how components are deployed to it.

Nodes represent servers, containers, or cloud services. Artifacts (your application components) are placed on nodes.

A deployment diagram might show three EC2 instances running the application, two RDS instances for the database, and an S3 bucket for file storage, all within a VPC.

**KEY TAKEAWAYS**

* Class diagrams show static structure: classes, attributes, methods, and relationships. They are the most commonly used UML diagram in OOD interviews. * Sequence diagrams show dynamic interaction: how objects communicate over time for a specific scenario. They reveal chatty designs and missing error paths. * Activity diagrams model workflows with decision points and parallel paths. Use case diagrams define system scope from the user's perspective.

* State machine diagrams capture object lifecycle with states and transitions. Essential for any entity with a complex lifecycle (orders, payments, tickets). * Component and deployment diagrams communicate architecture and infrastructure at a high level. Useful for stakeholder communication and operational documentation.

## Modularity & Interfaces

Large systems are not built as one giant block of code. They are decomposed into modules, each responsible for a specific capability.

How you draw the boundaries between modules, define their interfaces, and manage their dependencies determines whether your codebase grows gracefully or collapses into an unmaintainable mess.

**Module Decomposition Strategies**

A module is a self-contained unit of code with a well-defined interface and a clear responsibility. It can be a package, a library, a namespace, or a microservice depending on the scale you are working at.

#### Decompose by Feature/domain

Each module handles a specific business domain: users, orders, payments, notifications, inventory. This aligns modules with team ownership and business capabilities.

Changes to the payment flow are isolated to the payments module.

#### Decompose by Layer

Each module handles a specific technical layer: presentation, business logic, data access.

This works well for smaller applications but scales poorly because a single feature change (adding a discount field) touches all three layers.

#### Decompose by Volatility

Separate parts of the system that change frequently from parts that are stable.

The core domain logic (how orders work) changes less often than integration adapters (how you connect to a specific payment provider).

Isolating volatile code into its own module protects stable code from unnecessary churn.

The best decomposition usually combines strategies.

Top-level modules follow domain boundaries (orders, payments, users).

Within each domain module, code is organized by layer (API, service, repository).

Volatile integration code is isolated behind interfaces.

**Interface Design and Contracts**

An interface is the contract between a module and its consumers. It defines what the module provides (methods, data structures, events) and what it requires (input parameters, preconditions).

A well-designed interface hides internal complexity and remains stable even when the implementation changes.

Good interface design follows several principles.

* Minimize the surface area: Expose only what consumers need. Every public method is a commitment. Once consumers depend on it, changing it is expensive. Start with the smallest possible interface and expand only when needed. * Use consistent abstractions: If one method returns a `User` object and another returns a `HashMap<String, Object>` for the same data, the interface is inconsistent and confusing. Consistent return types, naming conventions, and error handling patterns make interfaces predictable.

* Design for the caller, not the implementer: The interface should make sense from the perspective of the code that uses it, not from the perspective of the code that implements it. If the internal data model uses fields named `usr_nm` and `usr_eml`, the public interface should expose `userName` and `userEmail`. * Version interfaces when breaking changes are necessary: If you need to add a required parameter to a method, create a v2 of the interface rather than breaking all existing callers. Deprecate v1 with a migration timeline.

**Dependency Injection and IoC Containers**

Dependency Injection (DI) is a technique where an object receives its dependencies from the outside rather than creating them internally.

Without DI: `class OrderService { private db = new PostgresDatabase(); }`. The OrderService is permanently coupled to PostgreSQL.

Testing it requires a real PostgreSQL instance.

With DI: `class OrderService { constructor(db: Database) { this.db = db; } }`.

The OrderService receives any object that implements the `Database` interface. In production, you inject `PostgresDatabase`.

In tests, you inject `InMemoryDatabase` or a mock. The OrderService does not know or care which implementation it receives.

DI is the practical application of the Dependency Inversion Principle (the "D" in SOLID).

High-level modules depend on abstractions, and the concrete implementations are injected at composition time.

IoC (Inversion of Control) containers automate dependency injection.

Instead of manually wiring dependencies (`new OrderService(new PostgresDatabase(new ConnectionPool(config)))`), the container reads configuration (annotations, XML, or code) and automatically creates objects with their dependencies injected. Spring (Java), .NET Dependency Injection, and Dagger (Android) are common IoC containers.

In dynamic languages, DI is often achieved through simpler mechanisms (constructor parameters, module imports) without a formal container.

The benefit of DI goes beyond testing.

It makes modules independently deployable (swap one implementation for another without changing the consumer), independently configurable (inject different settings for different environments), and independently evolvable (change the internal implementation without touching the interface).

**Beginner Mistake to Avoid**

New engineers sometimes create interfaces for everything, even when there is only one implementation and no foreseeable reason for a second one.

An `IUserService` interface with a single `UserServiceImpl` class adds indirection without benefit.

Create an interface when there is a genuine need for multiple implementations (different databases, different notification channels), when you need to inject a mock for testing, or when the module boundary is a critical isolation point (like between microservices).

For internal classes with one implementation, a concrete class is fine. You can always extract an interface later if the need arises (YAGNI).

Interview-Style Question

> Q: You are designing a report generation system that needs to produce reports in PDF, CSV, and Excel formats. How do you structure the code?

> A: Separate the report logic from the output format using dependency injection and the Strategy pattern. Define a `ReportFormatter` interface with a `format(reportData)` method. Implement `PDFFormatter`, `CSVFormatter`, and `ExcelFormatter`. The `ReportGenerator` class takes a `ReportFormatter` as a constructor parameter. It queries data, applies business rules, and delegates formatting to the injected formatter. To generate a PDF: `new ReportGenerator(new PDFFormatter()).generate()`. To add a new format later (like HTML), create `HTMLFormatter` and inject it. The `ReportGenerator` code does not change. This follows SRP (generation logic is separate from formatting logic), OCP (new formats are added without modifying existing code), and DIP (the generator depends on the `ReportFormatter` abstraction, not on any specific format).

_Report Generator Class Diagram_

### KEY TAKEAWAYS

* Decompose modules by domain (features and business capabilities) for large systems. Use layer-based decomposition within modules for internal organization.

* Design interfaces for the caller, not the implementer. Minimize surface area. Keep abstractions consistent. Version interfaces when breaking changes are needed. * Dependency Injection lets objects receive their dependencies from outside, enabling testability, flexibility, and loose coupling. * IoC containers automate dependency wiring in large applications. In smaller applications or dynamic languages, manual injection through constructors is sufficient. * Do not create interfaces for everything. Create them when there is a genuine need for abstraction: multiple implementations, testability, or critical module boundaries.