Back to Questions
03

Design Principles

SOLID, DRY, KISS, and clean code practices

Difficulty:
Session: 0 asked
1.

Single Responsibility: What constitutes a "reason to change"?

What is the Single Responsibility Principle? What constitutes a "reason to change"?

Junior

SRP: A class should have only one reason to change - meaning it should have only one job or responsibility.

"Reason to change" = Different Actor/Stakeholder

Robert Martin clarifies: A reason to change is tied to a particular actor or group of stakeholders.

Bad Example:

class Employee {
    // HR changes this
    double calculatePay() { }

    // Accounting changes this
    String generateReport() { }

    // DBA changes this
    void save() { }
}

Three actors → Three reasons to change → Violates SRP

Good Example:

class Employee {
    private String name;
    private double hourlyRate;
}

class PayCalculator {
    double calculatePay(Employee e) { }  // HR owns this
}

class ReportGenerator {
    String generateReport(Employee e) { }  // Accounting owns this
}

class EmployeeRepository {
    void save(Employee e) { }  // IT/DBA owns this
}

Signs of SRP Violation:
1. Class has multiple public methods serving different purposes
2. Changes for one feature break another
3. Different teams need to modify same class
4. Class name includes "And" or "Manager" (UserAndOrderManager)
5. Long class with many imports

Benefits of SRP:
- Easier testing (focused tests)
- Better maintainability
- Reduced merge conflicts
- Clearer ownership

Key Points to Look For:
- Understands "actor" based reasoning
- Can identify violations
- Knows practical benefits

Follow-up: Can SRP lead to too many classes? How do you balance?

2.

Open/Closed: How to extend without modifying?

How does the Open/Closed Principle work? How do you extend behavior without modifying existing code?

Mid

OCP: Software entities should be open for extension, closed for modification.

Techniques to Achieve OCP:

1. Polymorphism / Strategy Pattern

// Closed for modification
class DiscountCalculator {
    double calculate(DiscountStrategy strategy, double price) {
        return strategy.apply(price);
    }
}

// Open for extension
interface DiscountStrategy {
    double apply(double price);
}

class PercentageDiscount implements DiscountStrategy {
    double apply(double price) { return price * 0.9; }
}

// Add new discount without modifying existing code
class BulkDiscount implements DiscountStrategy {
    double apply(double price) { return price * 0.8; }
}

2. Template Method Pattern

abstract class DataExporter {
    // Template - closed for modification
    final void export(Data data) {
        validate(data);
        String formatted = format(data);  // Hook
        write(formatted);
    }

    // Open for extension
    abstract String format(Data data);
}

class CsvExporter extends DataExporter {
    String format(Data data) { return toCsv(data); }
}

3. Decorator Pattern

interface Coffee {
    double cost();
}

class BasicCoffee implements Coffee {
    double cost() { return 2.0; }
}

// Extend without modifying BasicCoffee
class MilkDecorator implements Coffee {
    private Coffee coffee;
    double cost() { return coffee.cost() + 0.5; }
}

Before OCP (Bad):

class PaymentProcessor {
    void process(Payment p) {
        if (p.type.equals("CREDIT")) {
            // process credit
        } else if (p.type.equals("DEBIT")) {
            // process debit
        }
        // Adding PayPal requires modifying this class!
    }
}

Key Points to Look For:
- Knows multiple extension mechanisms
- Can refactor closed code to open
- Understands trade-offs (indirection cost)

Follow-up: When might OCP add unnecessary complexity?

3.

Liskov Substitution: What violations look like?

What is the Liskov Substitution Principle? Can you give examples of violations?

Mid

LSP: Subtypes must be substitutable for their base types without altering program correctness.

If S is a subtype of T, objects of type T can be replaced with objects of type S without breaking the program.

Classic Violation: Rectangle/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 = height = w; }  // Violation!
    @Override
    void setHeight(int h) { width = height = h; } // Violation!
}

// Client code breaks
void resize(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    assert r.area() == 50;  // Fails for Square!
}

Violation Patterns:

1. Throwing Unexpected Exceptions

class Bird {
    void fly() { }
}

class Penguin extends Bird {
    void fly() {
        throw new UnsupportedOperationException();  // Violation!
    }
}

2. Strengthening Preconditions

class Account {
    void withdraw(int amount) {  // amount > 0
        balance -= amount;
    }
}

class RestrictedAccount extends Account {
    void withdraw(int amount) {
        if (amount > 100) throw new Exception();  // Stricter!
        super.withdraw(amount);
    }
}

3. Weakening Postconditions

class Repository {
    User findById(int id) {  // Never returns null
        return users.get(id);
    }
}

class CachedRepository extends Repository {
    User findById(int id) {
        return cache.get(id);  // Might return null!
    }
}

How to Fix:
1. Use composition instead of inheritance
2. Design better abstractions
3. Follow "IS-A" strictly

// Better design
interface Shape {
    int area();
}

class Rectangle implements Shape { }
class Square implements Shape { }  // Not a subtype of Rectangle

Key Points to Look For:
- Knows classic Rectangle/Square example
- Understands behavioral contracts
- Can identify real-world violations

Follow-up: How does LSP relate to Design by Contract?

4.

Interface Segregation: Fat interfaces problem

What is the Interface Segregation Principle? What problems do "fat interfaces" cause?

Mid

ISP: Clients should not be forced to depend on interfaces they don't use. Prefer many specific interfaces over one general interface.

Fat Interface Problem:

// Fat interface - forces all methods on implementers
interface Worker {
    void work();
    void eat();
    void sleep();
    void attendMeeting();
    void writeReport();
}

class HumanWorker implements Worker {
    void work() { }
    void eat() { }
    void sleep() { }
    void attendMeeting() { }
    void writeReport() { }
}

class RobotWorker implements Worker {
    void work() { }
    void eat() { throw new UnsupportedOperationException(); }  // Forced!
    void sleep() { throw new UnsupportedOperationException(); } // Forced!
    void attendMeeting() { }
    void writeReport() { }
}

Problems:
1. Implementers forced to stub unused methods
2. Changes to interface affect all implementers
3. Harder to test (mock entire interface)
4. Violation of SRP at interface level

Better Design:

interface Workable {
    void work();
}

interface Feedable {
    void eat();
}

interface Restable {
    void sleep();
}

interface Meetable {
    void attendMeeting();
}

class HumanWorker implements Workable, Feedable, Restable, Meetable {
    // Implements all
}

class RobotWorker implements Workable, Meetable {
    // Only what it needs
}

Real-World Example:

// Bad: Repository doing too much
interface Repository<T> {
    T findById(Long id);
    List<T> findAll();
    void save(T entity);
    void delete(T entity);
    void bulkInsert(List<T> entities);
    List<T> findByCustomQuery(String query);
    void executeStoredProcedure(String name);
}

// Good: Segregated
interface ReadRepository<T> {
    T findById(Long id);
    List<T> findAll();
}

interface WriteRepository<T> {
    void save(T entity);
    void delete(T entity);
}

interface BatchRepository<T> {
    void bulkInsert(List<T> entities);
}

Key Points to Look For:
- Can identify fat interfaces
- Knows to segregate by client needs
- Understands testing benefits

Follow-up: How does ISP relate to the Single Responsibility Principle?

5.

Dependency Inversion: High-level vs low-level modules

What is Dependency Inversion? Explain high-level vs low-level modules.

Mid

DIP:
1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
2. Abstractions should not depend on details. Details should depend on abstractions.

Without DIP (Tight Coupling):

// Low-level module (detail)
class MySQLDatabase {
    void insert(String data) { }
}

// High-level module (policy/business logic)
class UserService {
    private MySQLDatabase database;  // Depends on concrete!

    void createUser(User user) {
        database.insert(user.toString());
    }
}

Problems:
- Can't switch databases without changing UserService
- Hard to test (need real MySQL)
- High-level depends on low-level

With DIP (Inverted Dependency):

// Abstraction (owned by high-level module)
interface Database {
    void insert(String data);
}

// Low-level depends on abstraction
class MySQLDatabase implements Database {
    void insert(String data) { /* MySQL specific */ }
}

class PostgreSQLDatabase implements Database {
    void insert(String data) { /* PostgreSQL specific */ }
}

// High-level depends on abstraction
class UserService {
    private Database database;  // Interface, not concrete!

    UserService(Database database) {
        this.database = database;
    }

    void createUser(User user) {
        database.insert(user.toString());
    }
}

Dependency Direction:

Without DIP:
UserService → MySQLDatabase

With DIP:
UserService → Database ← MySQLDatabase

Key Insight: The interface should be owned/defined by the high-level module based on its needs, not by the low-level module.

Real Examples:
- Repository pattern (business logic doesn't know storage)
- Plugin architectures
- Dependency Injection frameworks

Key Points to Look For:
- Understands direction of dependency
- Knows who "owns" the abstraction
- Can explain with practical example

Follow-up: How do Dependency Injection frameworks implement DIP?

6.

How do SOLID principles relate to each other?

How are the SOLID principles connected? Do they support each other?

Senior

SOLID principles are interconnected and reinforce each other:

SRP ↔ ISP:
Both focus on cohesion and single purpose.
- SRP: Classes should have one responsibility
- ISP: Interfaces should have one responsibility
- Violating ISP often means violating SRP

OCP ↔ DIP:
DIP enables OCP.

// DIP: Depend on abstraction
class OrderProcessor {
    private PaymentGateway gateway;  // Interface
}

// OCP: Extend by adding new implementations
class StripeGateway implements PaymentGateway { }
class PayPalGateway implements PaymentGateway { }

LSP ↔ OCP:
LSP violations break OCP.

// If Square violates LSP for Rectangle,
// code using Rectangle can't be closed for modification
void process(Rectangle r) {
    r.setWidth(5);
    r.setHeight(10);
    // Must add if (r instanceof Square) check = OCP violation
}

ISP ↔ LSP:
ISP helps achieve LSP.

// Fat interface forces violations
interface Bird { void fly(); void walk(); }
class Penguin implements Bird {
    void fly() { throw new Exception(); }  // LSP violation
}

// Segregated interfaces avoid this
interface Walkable { void walk(); }
interface Flyable { void fly(); }
class Penguin implements Walkable { }  // No violation

All Support Testability:
- SRP: Focused, easy-to-test classes
- OCP: Test without modifying code
- LSP: Substitutable mocks
- ISP: Minimal mock setup
- DIP: Inject test doubles

Trade-offs:
- More classes/interfaces (indirection)
- Over-engineering risk
- Learning curve

Balance: Apply where change is expected, not everywhere.

Key Points to Look For:
- Sees connections between principles
- Understands mutual reinforcement
- Aware of over-application risk

Follow-up: Which principle would you prioritize if you had to choose one?

7.

Real-world SOLID violation examples and fixes

Give examples of SOLID violations you might see in real codebases and how to fix them.

Senior

1. SRP Violation: God Service

// Before: Does everything
class UserService {
    void createUser() { }
    void sendWelcomeEmail() { }
    void validatePassword() { }
    void generateReport() { }
    void exportToCSV() { }
}

// After: Separated concerns
class UserService { void createUser() { } }
class EmailService { void sendWelcomeEmail() { } }
class PasswordValidator { void validate() { } }
class UserReportService { void generateReport() { } }

2. OCP Violation: Switch/If Chain

// Before: Must modify for new types
double calculateShipping(Order order) {
    switch (order.type) {
        case "STANDARD": return 5.0;
        case "EXPRESS": return 15.0;
        case "OVERNIGHT": return 25.0;
        // Add new type = modify this method
    }
}

// After: Open for extension
interface ShippingStrategy {
    double calculate(Order order);
}

class StandardShipping implements ShippingStrategy { }
class ExpressShipping implements ShippingStrategy { }
// Add new strategy without modification

3. LSP Violation: Null Return Instead of Exception

// Before: Subclass changes contract
class BaseRepository {
    User findById(int id) {
        if (!exists(id)) throw new NotFoundException();
        return fetch(id);
    }
}

class CachedRepository extends BaseRepository {
    @Override
    User findById(int id) {
        return cache.get(id);  // Returns null! Different contract
    }
}

// After: Consistent contract
class CachedRepository extends BaseRepository {
    @Override
    User findById(int id) {
        User user = cache.get(id);
        if (user == null) {
            user = super.findById(id);
            cache.put(id, user);
        }
        return user;  // Same contract
    }
}

4. ISP Violation: Framework Interface

// Before: Must implement unused methods
interface DataProcessor {
    void onStart();
    void onData(byte[] data);
    void onComplete();
    void onError(Exception e);
    void onProgress(int percent);
}

// After: Segregated + defaults
interface DataProcessor {
    void onData(byte[] data);
    default void onStart() { }
    default void onComplete() { }
    default void onError(Exception e) { throw e; }
}

5. DIP Violation: Hard-coded Dependencies

// Before: Creates own dependencies
class OrderController {
    private OrderService service = new OrderService();
    private Logger logger = new FileLogger();
}

// After: Injected dependencies
class OrderController {
    private final OrderService service;
    private final Logger logger;

    OrderController(OrderService service, Logger logger) {
        this.service = service;
        this.logger = logger;
    }
}

Key Points to Look For:
- Recognizes real-world patterns
- Provides practical fixes
- Considers migration path

Follow-up: How would you introduce these changes incrementally in a legacy codebase?


Other Principles

8.

DRY: When does DRY become harmful?

When can the DRY (Don't Repeat Yourself) principle become harmful?

Mid

DRY: Every piece of knowledge should have a single, unambiguous representation.

When DRY Helps:
- Business logic duplication
- Configuration values
- Algorithms

When DRY Harms:

1. Accidental vs Essential Duplication

Our Definition of Done:
☐ Code reviewed and approved
☐ Unit tests written and passing
☐ Integration tests passing
☐ No critical/high bugs
☐ Documentation updated
☐ Deployed to staging
☐ QA sign-off
☐ Product Owner accepted

2. Wrong Abstraction

Story: "User can reset password"

Acceptance Criteria:
- User can request reset via email
- Reset link expires in 24 hours
- Password must meet security requirements
- User receives confirmation email
- Invalid links show appropriate error

3. Coupling Unrelated Things

Story: "User can reset password"

Acceptance Criteria: (What)
✓ Reset via email
✓ Link expires in 24 hours
✓ Security requirements met

Definition of Done: (How)
✓ Code reviewed
✓ Tests written
✓ Deployed to staging
✓ PO accepted

Both must be met for story to be "Done"

The Rule of Three:
Don't abstract until you see something three times.

Better Principles:
- "Duplication is far cheaper than the wrong abstraction" - Sandi Metz
- AHA: Avoid Hasty Abstractions
- WET: Write Everything Twice (then abstract)

Key Points to Look For:
- Understands accidental vs essential duplication
- Knows wrong abstraction problem
- Can identify when to duplicate

Follow-up: How do microservices handle DRY across service boundaries?

9.

KISS: Simplicity vs functionality trade-offs

How do you balance the KISS principle with feature requirements?

Junior

KISS: Keep It Simple, Stupid - Prefer simple solutions over complex ones.

Signs of Over-Complexity:

// Over-engineered for simple requirement
interface DataSource<T> { }
abstract class AbstractDataSource<T> implements DataSource<T> { }
class CachingDataSourceDecorator<T> extends AbstractDataSource<T> { }
class LoggingDataSourceDecorator<T> extends AbstractDataSource<T> { }

// Requirement: Read a config file
// Simple solution:
Properties props = new Properties();
props.load(new FileReader("config.properties"));

Balancing Trade-offs:

1. Solve Current Problem First

// Don't: Build for imaginary future
class MessageSender {
    void send(Message m, Channel c, Retry r, Transform t) { }
}

// Do: Solve what's needed now
class EmailSender {
    void send(String to, String body) { }
}

2. Add Complexity Only When Needed

// Start simple
String config = readFile("config.json");

// Add complexity when requirements demand
ConfigLoader loader = new ConfigLoader(cache, validator, fallback);

3. Prefer Readable Over Clever

// Clever but complex
return items.stream()
    .filter(x -> x != null)
    .map(this::transform)
    .flatMap(Collection::stream)
    .collect(groupingBy(Item::type, mapping(Item::value, toList())));

// Simple and clear
List<Item> result = new ArrayList<>();
for (Item item : items) {
    if (item != null) {
        result.addAll(transform(item));
    }
}
return groupByType(result);

When Complexity is Justified:
- Performance requirements demand it
- Requirement is inherently complex
- Abstraction genuinely reduces cognitive load
- Supporting multiple use cases

Key Points to Look For:
- Recognizes over-engineering
- Prioritizes current requirements
- Values readability

Follow-up: How do you explain to stakeholders that simple takes longer?

10.

YAGNI: How to balance with extensibility?

How do you balance YAGNI (You Aren't Gonna Need It) with designing for extensibility?

Mid

YAGNI: Don't add functionality until it's actually needed.

YAGNI vs OCP Tension:
- YAGNI: Don't build for hypothetical futures
- OCP: Design for extension

Resolution: Extension Points, Not Features

// Bad: Building unused features (YAGNI violation)
class ReportGenerator {
    void generatePDF() { }
    void generateExcel() { }  // Not requested
    void generateWord() { }   // Not requested
    void generateHTML() { }   // Not requested
}

// Good: Extension point without implementations
interface ReportFormat {
    byte[] generate(Data data);
}

class PDFFormat implements ReportFormat { }  // Only what's needed

// Later, when needed:
class ExcelFormat implements ReportFormat { }  // Easy to add

Guidelines:

Apply YAGNI to:
- Specific features
- Concrete implementations
- Premature optimizations
- Speculative requirements

Invest in:
- Clean interfaces (low cost, high value)
- Separation of concerns
- Dependency injection structure
- Testability

Practical Test:
Ask: "Will this require major refactoring to add later?"
- Yes → Invest now
- No → Wait (YAGNI)

// Will require major refactoring: Database abstraction
// Invest in interface early
interface Repository { }

// Won't require refactoring: Add export format
// Wait until needed (YAGNI)

Key Points to Look For:
- Distinguishes features from architecture
- Knows when to invest early
- Practical judgment

Follow-up: Give an example where YAGNI led to technical debt.

11.

Composition over Inheritance: practical examples

Why prefer composition over inheritance? Give practical examples.

Mid

Composition: Build complex objects by combining simpler ones (HAS-A).
Inheritance: Create new classes from existing ones (IS-A).

Why Composition:

1. Flexibility at Runtime

// Inheritance: Fixed at compile time
class Duck extends FlyingAnimal { }  // Can't change

// Composition: Changeable at runtime
class Duck {
    private FlyBehavior flyBehavior;

    void setFlyBehavior(FlyBehavior fb) {
        this.flyBehavior = fb;  // Can change!
    }
}

duck.setFlyBehavior(new CannotFly());  // Injured duck

2. Multiple Behaviors

// Inheritance: Single inheritance limit
class RoboDuck extends Robot { }  // Can't also extend Duck

// Composition: Combine freely
class RoboDuck {
    private FlyBehavior fly;
    private QuackBehavior quack;
    private ChargeBehavior charge;
}

3. Avoid Fragile Base Class

// Inheritance: Base change breaks subclass
class CountingHashSet extends HashSet {
    private int addCount = 0;

    @Override
    boolean add(Object o) {
        addCount++;
        return super.add(o);
    }

    @Override
    boolean addAll(Collection c) {
        addCount += c.size();
        return super.addAll(c);  // Calls add() internally!
    }
}

// Composition: Isolated from implementation
class CountingSet {
    private Set set = new HashSet();
    private int addCount = 0;

    boolean add(Object o) {
        addCount++;
        return set.add(o);
    }
}

4. Better Testing

// Composition: Easy to mock
class OrderService {
    private PaymentGateway gateway;  // Inject mock

    OrderService(PaymentGateway gateway) {
        this.gateway = gateway;
    }
}

When Inheritance is OK:
- True IS-A relationship
- Controlled hierarchy (you own both)
- Template Method pattern
- Framework requirements

Key Points to Look For:
- Knows fragile base class problem
- Understands flexibility benefits
- Can give practical examples

Follow-up: How does the Strategy pattern use composition?

12.

Law of Demeter: "Don't talk to strangers" explained

What is the Law of Demeter? What problems does it solve?

Mid

Law of Demeter (Principle of Least Knowledge):
A method should only call methods on:
1. Itself (this)
2. Its parameters
3. Objects it creates
4. Its direct components

Don't: Chain through objects you don't directly know.

Violation:

wzxhzdk:0

Fixed:

wzxhzdk:1

Why It Matters:

1. Reduced Coupling

wzxhzdk:2

2. Easier Changes

wzxhzdk:3

3. Better Encapsulation

wzxhzdk:4

Not a Violation:

wzxhzdk:5

The key: Each call returns the same type (self), not navigating through different types.

Key Points to Look For:
- Identifies train wreck code
- Knows exception for fluent APIs
- Understands coupling reduction

Follow-up: How does LoD relate to feature envy code smell?

13.

Separation of Concerns in layered architecture

How does Separation of Concerns apply to layered architecture?

Mid

Separation of Concerns: Divide program into distinct sections, each addressing a separate concern.

Typical Layered Architecture:

┌──────────────────────────┐
│   Presentation Layer     │  UI, API controllers
├──────────────────────────┤
│   Business Logic Layer   │  Domain services, rules
├──────────────────────────┤
│   Data Access Layer      │  Repositories, ORM
├──────────────────────────┤
│   Database               │  Storage
└──────────────────────────┘

Each Layer's Concern:

Presentation:
- HTTP handling, request/response
- Input validation (format)
- View rendering

@RestController
class UserController {
    @PostMapping("/users")
    Response createUser(@Valid UserDTO dto) {
        User user = userService.create(dto);
        return Response.created(user.getId());
    }
}

Business Logic:
- Business rules
- Domain validation
- Orchestration

class UserService {
    User create(UserDTO dto) {
        validateBusinessRules(dto);
        User user = new User(dto);
        userRepository.save(user);
        eventPublisher.publish(new UserCreated(user));
        return user;
    }
}

Data Access:
- Persistence logic
- Query building
- Transaction management

class UserRepository {
    void save(User user) {
        entityManager.persist(user);
    }
}

Benefits:
1. Independent testing - Mock lower layers
2. Replaceability - Swap implementations
3. Team organization - Frontend/Backend split
4. Maintainability - Changes localized

Violations:

// Controller with business logic
@PostMapping("/users")
Response createUser(UserDTO dto) {
    // Business rule in controller!
    if (userRepo.countByEmail(dto.email) > 0) {
        throw new DuplicateException();
    }
    // SQL in controller!
    jdbc.execute("INSERT INTO users...");
}

Key Points to Look For:
- Knows typical layer responsibilities
- Can identify violations
- Understands testing benefits

Follow-up: How does Clean Architecture differ from traditional layered architecture?

14.

Principle of Least Astonishment

What is the Principle of Least Astonishment (Surprise)? Give examples.

Junior

POLA: Software should behave in a way that least surprises users/developers.

Code should do what it looks like it does.

Violations:

1. Unexpected Side Effects

// getName() shouldn't modify state
String getName() {
    accessCount++;  // Surprise!
    lastAccessed = new Date();  // Surprise!
    return name;
}

// Better: Make side effects explicit
String getNameAndLogAccess() { }

2. Inconsistent Returns

// Sometimes returns null, sometimes empty list
List<User> findByRole(String role) {
    if (role == null) return null;  // Surprise!
    // ... returns empty list if no users found
}

// Better: Consistent behavior
List<User> findByRole(String role) {
    if (role == null) return Collections.emptyList();
    // ... always returns list (possibly empty)
}

3. Misleading Names

// "delete" suggests it removes
void deleteUser(User user) {
    user.setStatus("INACTIVE");  // Surprise! Doesn't delete
}

// Better
void deactivateUser(User user) {
    user.setStatus("INACTIVE");
}

4. Order-Dependent API

// Must call in specific order
Report report = new Report();
report.addData(data);      // Must be first
report.setFormat("PDF");   // Must be second
report.generate();         // Must be last

// Better: Builder pattern
Report report = Report.builder()
    .data(data)
    .format("PDF")
    .build();

Guidelines:
- Follow conventions (getters don't modify)
- Names match behavior
- Consistent patterns throughout
- Document surprising behavior

Key Points to Look For:
- Recognizes astonishing code
- Values predictability
- Thinks from user perspective

Follow-up: How do conventions differ between languages/frameworks?

15.

Fail Fast principle and its benefits

What is the Fail Fast principle? What are its benefits?

Junior

Fail Fast: Detect and report errors as soon as they occur, rather than continuing with invalid state.

Without Fail Fast:

void processOrder(Order order) {
    // Null check missing - fails later
    double total = order.getItems().stream()
        .mapToDouble(Item::getPrice)
        .sum();  // NullPointerException deep in processing

    // Error occurs far from cause
}

With Fail Fast:

void processOrder(Order order) {
    Objects.requireNonNull(order, "Order cannot be null");
    Objects.requireNonNull(order.getItems(), "Order must have items");
    if (order.getItems().isEmpty()) {
        throw new IllegalArgumentException("Order must have at least one item");
    }

    // Now safe to proceed
    double total = order.getItems().stream()
        .mapToDouble(Item::getPrice)
        .sum();
}

Benefits:

1. Easier Debugging

// Fail fast: Error at source
"Order cannot be null at OrderService.processOrder:12"

// Without: Error far from cause
"NullPointerException at PriceCalculator.calculate:89"

2. Prevents Corrupt State

void transferMoney(Account from, Account to, double amount) {
    // Fail fast: Validate before any changes
    if (amount <= 0) throw new IllegalArgumentException();
    if (from.getBalance() < amount) throw new InsufficientFundsException();

    from.debit(amount);
    to.credit(amount);
}

3. Clearer Contracts

// Preconditions make requirements explicit
public void setAge(int age) {
    if (age < 0 || age > 150) {
        throw new IllegalArgumentException("Age must be 0-150");
    }
    this.age = age;
}

Implementation Patterns:

// Java Objects utilities
Objects.requireNonNull(obj);
Objects.requireNonNullElse(obj, defaultValue);

// Guava Preconditions
Preconditions.checkArgument(count > 0);
Preconditions.checkState(initialized);

Key Points to Look For:
- Understands early detection value
- Knows validation patterns
- Can explain debugging benefit

Follow-up: When might fail-fast not be appropriate?

16.

Convention over Configuration

What is Convention over Configuration? Give examples from frameworks.

Mid

CoC: Sensible defaults reduce the amount of explicit configuration needed. Only configure what differs from convention.

Example: Spring Boot

Technical: "The cache reduces database load"
Analogy: "Like a library's front desk keeping popular books
         handy instead of fetching from warehouse each time"

Technical: "API rate limiting"
Analogy: "Like a store limiting items per customer during a sale"

Example: Ruby on Rails

Not: "We need to refactor the authentication module"
But: "We need to improve login security and speed.
     This will reduce login failures by 50%."

Example: Maven

Replace: "We're implementing a microservices architecture
          with Kubernetes orchestration"

With: "We're splitting our system into smaller,
      independent pieces that are easier to update
      and scale as we grow"

Benefits:
1. Less boilerplate - New projects start fast
2. Consistency - Teams follow same patterns
3. Discovery - Know where to look
4. Less decisions - Fewer bike-shedding debates

Trade-offs:
1. Magic - Hard to understand how things work
2. Learning curve - Must learn conventions
3. Inflexibility - Hard to deviate
4. Implicit behavior - Harder to debug

Balance:

Before/After diagrams
Simple flowcharts
Metrics graphs
Timeline visualizations

Key Points to Look For:
- Knows framework examples
- Understands trade-offs
- Can identify when to override

Follow-up: How does CoC relate to Principle of Least Astonishment?

17.

Tell, Don't Ask principle

What is the "Tell, Don't Ask" principle? How does it improve code?

Mid

Tell, Don't Ask: Tell objects what to do, don't ask for their state and decide for them.

Asking (Bad):

// Asking for state, deciding externally
if (account.getBalance() >= amount) {
    account.setBalance(account.getBalance() - amount);
} else {
    throw new InsufficientFundsException();
}

// Problem: Logic is outside the object
// Every caller must implement same check

Telling (Good):

// Tell the object what to do
account.withdraw(amount);

// Inside Account:
void withdraw(double amount) {
    if (balance < amount) {
        throw new InsufficientFundsException();
    }
    balance -= amount;
}

Why It Matters:

1. Encapsulation

// Bad: Exposes internals
if (car.getFuelLevel() > 0 && car.getEngine().isWorking()) {
    car.getEngine().start();
}

// Good: Hides internals
car.start();  // Car decides how

2. Single Point of Change

// If validation rules change, only Account changes
// Not every caller

3. Avoids Feature Envy

// Feature envy: Method more interested in other class
class OrderProcessor {
    void process(Order order) {
        // Asking Order lots of questions
        if (order.getStatus().equals("PENDING") &&
            order.getPayment().isValid() &&
            order.getItems().stream().allMatch(Item::inStock)) {
            // Process...
        }
    }
}

// Better: Tell Order to validate itself
class OrderProcessor {
    void process(Order order) {
        if (order.isReadyToProcess()) {
            // Process...
        }
    }
}

Exceptions:
- DTOs (data transfer, no behavior)
- Query methods (asking is the purpose)
- Debugging/logging

Key Points to Look For:
- Recognizes "asking" code smell
- Understands encapsulation benefit
- Can refactor examples

Follow-up: How does this relate to the Law of Demeter?


Clean Code

18.

What makes code "clean"?

What characteristics make code "clean"?

Junior

Clean Code Characteristics:

1. Readable

// Clean: Reads like prose
if (user.canAccessResource(resource)) {
    resource.grantAccess(user);
}

// Not clean: Requires mental parsing
if (u.acl.contains(r.id) && !r.locked && u.exp > now()) { }

2. Simple
- Does one thing well
- No unnecessary complexity
- Easy to understand

3. Testable
- Dependencies injectable
- Single responsibility
- Deterministic behavior

4. Intentional

// Clean: Name reveals intent
int daysSinceCreation;
boolean isEligibleForDiscount;

// Not clean: Cryptic
int d;
boolean flag2;

5. Has No Duplication
- DRY where appropriate
- Abstractions for repeated patterns

6. Minimal
- No dead code
- No unnecessary comments
- No premature optimization

7. Follows Standards
- Consistent formatting
- Team conventions
- Language idioms

Grady Booch's Definition:
"Clean code reads like well-written prose."

Robert Martin's Definition:
"Clean code always looks like it was written by someone who cares."

Bjarne Stroustrup (C++ creator):
"Clean code does one thing well."

Signs of Clean Code:
- New team member understands quickly
- Easy to modify
- Tests pass
- No surprises

Key Points to Look For:
- Multiple characteristics mentioned
- Examples for each
- Quotes from thought leaders

Follow-up: How do you maintain clean code in a fast-paced environment?

19.

Meaningful naming conventions

What makes a good variable/function/class name?

Junior

Naming Guidelines:

1. Reveal Intent

// Bad
int d; // elapsed time in days

// Good
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;

2. Avoid Disinformation

// Bad: Not actually a List
Map<String, User> userList;

// Good
Map<String, User> usersByName;

3. Make Meaningful Distinctions

// Bad: Noise words
getAccount();
getAccountData();
getAccountInfo();

// Good: Distinct meaning
getAccountBalance();
getAccountHistory();
getAccountOwner();

4. Use Pronounceable Names

// Bad
private Date genymdhms;  // generation year, month, day, hour, minute, second

// Good
private Date generationTimestamp;

5. Use Searchable Names

// Bad: Magic number
if (status == 4) { }

// Good: Searchable constant
if (status == STATUS_ACTIVE) { }

6. Avoid Encodings

// Bad: Hungarian notation
String strName;
int iCount;

// Good
String name;
int count;

7. Class Names = Nouns

// Good
Customer, Account, AddressParser

// Bad
Manager, Processor, Data (too generic)

8. Method Names = Verbs

// Good
postPayment(), deletePage(), calculateTotal()

// Bad
payment(), page(), total()

Length Guidelines:
- Loop variables: short (i, j)
- Local variables: medium
- Instance variables: descriptive
- Global constants: very descriptive

Key Points to Look For:
- Multiple principles mentioned
- Good and bad examples
- Understands context matters

Follow-up: How do naming conventions differ between languages?

20.

Function length and single responsibility

How long should functions be? How do you know when to split them?

Junior

Bad:  "You always make this mistake"
Good: "This pattern can lead to memory leaks"

Bad:  "Why did you do it this way?"
Good: "I'm curious about the approach here. What led to this design?"

Bad:  "This is wrong"
Good: "This query runs N+1 times. Consider using .includes(:orders)
       to eager load the association."

Bad:  "Clean this up"
Good: "This method is 80 lines. Could we extract the validation
       logic into a separate method for readability?"

Bad:  "Use const instead of let"
Good: "Using const here signals this value won't be reassigned,
       making the code easier to reason about."

Bad:  "Add error handling"
Good: "If the API call fails here, the user sees a blank page.
       Adding a try/catch with a fallback improves the experience."

Bad:  "This isn't efficient"
Good: "This works! For better performance with large datasets,
       you might consider using a Map for O(1) lookups instead
       of filtering the array. Happy to pair on this if helpful."

5. Use Questions, Not Commands:

Bad:  "Change this to async/await"
Good: "Have you considered using async/await here?
       It might make the error handling cleaner."

Bad:  "This function is too long"
Good: "What do you think about splitting this into smaller functions?
       It might be easier to test."

Comment Categories:

// Required - Must fix before merging
"This will cause a null pointer exception when user is undefined."

// Suggestion - Nice to have
"nit: Could rename 'data' to 'userProfile' for clarity"

// Question - Seeking understanding
"Question: I'm not familiar with this library. How does X work?"

// Praise - Acknowledge good work
"Nice! This is a clever solution to the caching problem."

Praise Template:

"I like how you [specific thing]. It [specific benefit]."

Examples:
"I like how you extracted this into a utility function.
 It makes it easy to test and reuse."

"Great job handling the edge case where the array is empty.
 That's easy to miss."

Tone Checklist:

☐ Would I be comfortable receiving this comment?
☐ Is it specific enough to be actionable?
☐ Did I explain why, not just what?
☐ Did I offer an alternative or help?
☐ Did I acknowledge what's working?

Key Points to Look For:
- Focuses on code, not person
- Provides specific, actionable feedback
- Explains reasoning
- Maintains positive tone

Follow-up: How do you handle when someone pushes back on your feedback?

21.

Comments: when are they necessary?

When should you write comments in code? When are they unnecessary?

Mid

Good Comments:

1. Legal Comments

// Copyright (c) 2024 Company. All rights reserved.

2. Explanation of Intent

// We use insertion sort here because the list is almost always
// sorted, making it O(n) in practice despite O(n²) worst case

3. Clarification

// Format: YYYY-MM-DD (ISO 8601)
String datePattern = "\\d{4}-\\d{2}-\\d{2}";

4. Warning of Consequences

// WARNING: Running this will delete all test data
// Only use in development environment
void resetDatabase() { }

5. TODO Comments (temporarily)

// TODO: Replace with proper caching after v2 release

6. Javadoc for Public APIs

/**
 * Calculates compound interest.
 * @param principal Initial amount
 * @param rate Annual interest rate (0.05 = 5%)
 * @param years Number of years
 * @return Final amount after compounding
 */
public double calculateCompoundInterest(...)

Bad Comments:

1. Redundant

// Increment counter
counter++;

2. Mandated Comments

/**
 * @param name The name
 * @return The value
 */
// Adds no information!

3. Journal Comments

// Changed by Bob on 2024-01-15
// Modified by Alice on 2024-02-20
// Use version control instead!

4. Commented-Out Code

// oldMethod();
// anotherOldMethod();
// Delete this - use version control

5. Comments Instead of Refactoring

// Bad: Comment explains confusing code
// Calculate user's effective permissions
int p = (u.r & g.r) | u.o;

// Good: Self-documenting
int effectivePermissions = combinePermissions(user, group);

Key Points to Look For:
- Distinguishes good from bad
- Prefers self-documenting code
- Knows when comments add value

Follow-up: How do you keep comments in sync with code?

22.

Code smells: identifying and refactoring

What are common code smells and how do you refactor them?

Mid

Code Smells and Refactorings:

1. Long Method

// Smell: Method doing too much
void processOrder(Order order) {
    // 200 lines of code
}

// Refactor: Extract methods
void processOrder(Order order) {
    validateOrder(order);
    calculateTotals(order);
    applyDiscounts(order);
    processPayment(order);
    sendConfirmation(order);
}

2. Large Class (God Class)

// Smell: Class with too many responsibilities
class UserManager {
    void createUser() { }
    void sendEmail() { }
    void generateReport() { }
    void processPayment() { }
}

// Refactor: Extract classes
class UserService { }
class EmailService { }
class ReportService { }

3. Feature Envy

// Smell: Method more interested in other class
class Order {
    void calculateShipping() {
        if (customer.getAddress().getCountry().equals("US")) {
            // Uses customer internals extensively
        }
    }
}

// Refactor: Move method
class Customer {
    boolean isInUS() { return address.getCountry().equals("US"); }
}

4. Primitive Obsession

// Smell: Using primitives for domain concepts
String phoneNumber;
int postalCode;
double price;

// Refactor: Create value objects
PhoneNumber phone;
PostalCode zip;
Money price;

5. Shotgun Surgery

// Smell: One change requires many class modifications
// Adding a field requires changing Controller, Service, DAO, DTO

// Refactor: Consolidate related logic

6. Data Clumps

// Smell: Same parameters always together
void createUser(String street, String city, String zip) { }
void updateAddress(String street, String city, String zip) { }

// Refactor: Create class
void createUser(Address address) { }

7. Switch Statements

// Smell: Same switch in multiple places
switch (type) {
    case "A": return handleA();
    case "B": return handleB();
}

// Refactor: Use polymorphism
handler.handle(type);

Key Points to Look For:
- Knows multiple smells
- Can suggest specific refactorings
- Understands root causes

Follow-up: How do you prioritize which code smells to fix first?

23.

Boy Scout Rule in practice

What is the Boy Scout Rule? How do you apply it in practice?

Junior

Boy Scout Rule: "Leave the code cleaner than you found it."

In Practice:

When Adding a Feature:

Read in order:
1. README - Project overview, setup instructions
2. Architecture docs - How pieces fit together
3. API documentation - Key interfaces
4. Recent design docs - Current thinking
5. Onboarding guide (if exists)

Small Improvements:
1. Rename unclear variable

# Clone, build, run locally
# Don't skip this - reveals assumptions

Ask: "What's the happy path?"
Run through it as a user
  1. Extract magic number
Identify:
- Entry points (main.py, index.js, App.java)
- Folder structure patterns
- Key abstractions/interfaces
- Configuration files
- Test locations
  1. Remove dead code
Good first tasks:
- Fix a small bug
- Add a log message
- Update documentation
- Write a missing test
- Small UI tweak

Each task teaches:
- How to find code
- How to test changes
- How to submit PRs
- Team norms and style
  1. Add missing null check
Look for:
- Code style conventions
- PR description format
- Review process
- Common patterns
  1. Improve formatting
Set breakpoints, step through code
"How does X actually work?"
Better than reading docs alone

Boundaries:
- Do: Small, safe improvements
- Don't: Massive refactoring in unrelated code
- Don't: Changes that need separate testing

Team Practice:

Meet with:
- Manager (expectations, priorities)
- Tech lead (architecture, history)
- Key team members (domain knowledge)
- Adjacent teams (dependencies)

Ask:
"What should I know that isn't documented?"
"What do you wish you knew when you started?"
"What's the biggest pain point?"

Benefits:
1. Gradual improvement - No big rewrite needed
2. Shared ownership - Everyone maintains quality
3. Prevents decay - Constant small fixes
4. Learning - Reading others' code

Challenges:
- Time pressure
- Code ownership sensitivity
- Unrelated changes in PR

Key Points to Look For:
- Understands incremental improvement
- Knows appropriate scope
- Can give concrete examples

Follow-up: How do you handle pushback on "unrelated" improvements in code review?