Episode 9 — System Design / 9.2 — Design Principles

9.2 — Exercise Questions: Design Principles

How to use this material:

  1. Try answering each question before looking at the referenced sub-topic file.
  2. Write actual code where indicated — don't just think through answers.
  3. Questions marked with a wrench require hands-on coding.
  4. Questions marked with a thought bubble are conceptual — explain in your own words.
  5. Check your answers against the sub-topic files linked in each section.

9.2.a — SOLID Principles (Q1–Q15)

Q1. (conceptual) In your own words, explain what SRP means. Why is "a class should do one thing" a misleading simplification?

Q2. (code) This Express route handler violates SRP. Identify the distinct responsibilities and refactor it into separate layers:

app.post('/api/products', async (req, res) => {
  if (!req.body.name || req.body.price <= 0) {
    return res.status(400).json({ error: 'Invalid product' });
  }
  const slug = req.body.name.toLowerCase().replace(/\s+/g, '-');
  await db.query('INSERT INTO products (name, price, slug) VALUES ($1, $2, $3)', 
    [req.body.name, req.body.price, slug]);
  await emailService.send(adminEmail, 'New Product', `Product ${req.body.name} added`);
  res.status(201).json({ name: req.body.name, slug });
});

Q3. (conceptual) How do you identify the "actors" or stakeholders behind a class to determine if SRP is violated?

Q4. (code) Refactor this function that violates OCP so that adding a new shape requires zero changes to existing code:

function calculateArea(shape: { type: string; width?: number; height?: number; radius?: number }) {
  switch (shape.type) {
    case 'rectangle': return shape.width! * shape.height!;
    case 'circle': return Math.PI * shape.radius! ** 2;
    case 'triangle': return 0.5 * shape.width! * shape.height!;
  }
}

Q5. (conceptual) How does the Strategy pattern relate to OCP? Give an example where adding a new strategy satisfies OCP.

Q6. (code) The following code violates LSP. Explain why and fix it:

class Bird {
  fly(): string { return 'Flying high'; }
  eat(): string { return 'Eating seeds'; }
}

class Penguin extends Bird {
  fly(): string { throw new Error('Penguins cannot fly'); }
}

function makeBirdFly(bird: Bird): string {
  return bird.fly();
}

Q7. (conceptual) What are the three rules of LSP regarding preconditions, postconditions, and invariants? Give an example violation of each.

Q8. (code) This interface violates ISP. Split it into focused interfaces and show how different classes would implement only what they need:

interface SmartDevice {
  call(number: string): void;
  sendText(number: string, message: string): void;
  browseWeb(url: string): void;
  takePhoto(): void;
  playMusic(track: string): void;
  makePayment(amount: number): void;
}

Q9. (conceptual) How does ISP relate to SRP? Can you have ISP without SRP? Can you have SRP without ISP?

Q10. (code) Refactor this code to follow DIP. The ReportGenerator should not depend on concrete PDFExporter and MySQLDataSource:

class ReportGenerator {
  private exporter = new PDFExporter();
  private dataSource = new MySQLDataSource();

  generate(): void {
    const data = this.dataSource.fetchData();
    this.exporter.export(data);
  }
}

Q11. (conceptual) Draw the dependency diagram for the code in Q10 before and after applying DIP. Which way do the arrows point?

Q12. (code) Write a mock implementation for the interfaces you created in Q10 and show how you would use them in a unit test.

Q13. (conceptual) A teammate says "SOLID is only for Java/C#. JavaScript doesn't need it because we have functions and closures." How do you respond?

Q14. (code) Take a real or imagined Express application with a "god route" (one route handler that does validation, auth check, business logic, database queries, and response formatting). Refactor it using at least three SOLID principles.

Q15. (conceptual) Rank the five SOLID principles from "most important in a small project" to "least important in a small project." Justify your ranking.


9.2.b — DRY, KISS, YAGNI, and Other Principles (Q16–Q30)

Q16. (conceptual) Two functions in your codebase look identical but serve different business purposes. Is this a DRY violation? Explain.

Q17. (code) Find the DRY violation in this code and fix it:

app.get('/api/users/:id', async (req, res) => {
  const user = await db.query('SELECT id, name, email, role FROM users WHERE id = $1', [req.params.id]);
  if (!user.rows[0]) return res.status(404).json({ error: 'User not found' });
  res.json(user.rows[0]);
});

app.put('/api/users/:id', async (req, res) => {
  const user = await db.query('SELECT id, name, email, role FROM users WHERE id = $1', [req.params.id]);
  if (!user.rows[0]) return res.status(404).json({ error: 'User not found' });
  // ... update logic
});

app.delete('/api/users/:id', async (req, res) => {
  const user = await db.query('SELECT id, name, email, role FROM users WHERE id = $1', [req.params.id]);
  if (!user.rows[0]) return res.status(404).json({ error: 'User not found' });
  // ... delete logic
});

Q18. (conceptual) Explain the "Rule of Three" for DRY. Why shouldn't you abstract on the second occurrence?

Q19. (code) Simplify this over-engineered code following KISS:

class StringReverserFactory {
  static createReverser(type: 'recursive' | 'iterative' | 'functional') {
    switch (type) {
      case 'recursive': return new RecursiveReverser();
      case 'iterative': return new IterativeReverser();
      case 'functional': return new FunctionalReverser();
    }
  }
}
// ... three reverser classes, each 20+ lines

Q20. (conceptual) Give three real-world examples where applying YAGNI would have saved a team significant time.

Q21. (code) You have this inheritance hierarchy. Refactor it to use composition instead:

class Vehicle {
  drive() { return 'Driving'; }
}
class ElectricVehicle extends Vehicle {
  charge() { return 'Charging'; }
}
class FlyingVehicle extends Vehicle {
  fly() { return 'Flying'; }
}
// How do you make a FlyingElectricVehicle?

Q22. (conceptual) Explain the Law of Demeter using the "one dot rule." What are the exceptions to this rule?

Q23. (code) This code violates the Law of Demeter. Refactor it:

function getDiscountedPrice(order: Order): number {
  const loyaltyLevel = order.getCustomer().getLoyaltyProgram().getCurrentLevel();
  const discount = order.getCustomer().getLoyaltyProgram().getDiscountRate(loyaltyLevel);
  return order.getTotal() * (1 - discount);
}

Q24. (conceptual) What is Separation of Concerns? How does it differ from SRP?

Q25. (code) This function violates Separation of Concerns. Identify the concerns and separate them:

async function handleRegistration(req: Request, res: Response) {
  const { email, password, name } = req.body;
  if (!email.includes('@')) return res.status(400).json({ error: 'Bad email' });
  if (password.length < 8) return res.status(400).json({ error: 'Short password' });
  const hash = await bcrypt.hash(password, 10);
  const user = await db.query('INSERT INTO users ...', [email, hash, name]);
  const token = jwt.sign({ id: user.id }, 'secret', { expiresIn: '1h' });
  await transporter.sendMail({ to: email, subject: 'Welcome!', html: '...' });
  res.status(201).json({ user: user.rows[0], token });
}

Q26. (conceptual) You are on a team of 2 building a personal project. Which principles do you prioritize? Which can you relax? What changes when the team grows to 10?

Q27. (conceptual) Describe a scenario where DRY and KISS conflict. Which would you prioritize and why?

Q28. (code) Your colleague wrote this "future-proof" cache system for a blog with 50 daily visitors. Critique it:

interface CacheDriver { /* ... */ }
interface CacheSerializer { /* ... */ }
interface CacheInvalidationStrategy { /* ... */ }
interface CacheWarmingStrategy { /* ... */ }
class CacheManager {
  constructor(
    driver: CacheDriver,
    serializer: CacheSerializer,
    invalidation: CacheInvalidationStrategy,
    warming: CacheWarmingStrategy
  ) { /* 200 lines */ }
}

Q29. (conceptual) What is the difference between "accidental duplication" and "true duplication"? How do you tell them apart?

Q30. (code) Write a practical example demonstrating when inheritance IS the right choice over composition. Explain why composition would be worse in this case.


9.2.c — Design Patterns (Q31–Q38)

Q31. (conceptual) What is the difference between a design pattern, an algorithm, and a library? Give one example of each.

Q32. (conceptual) Name the three categories of GoF patterns and explain the purpose of each with one pattern example per category.

Q33. (code) Implement the Singleton pattern for a database connection pool. Then show how the same effect is achieved "for free" in a Node.js module. Which approach do you prefer and why?

Q34. (conceptual) Your team is debating whether to use the Factory pattern for creating API responses. There are currently two response types: success and error. Is a Factory justified? At what point would it become justified?

Q35. (code) Implement a simple Decorator pattern that wraps a Logger with timestamp and prefix decorators. Show how to stack decorators.

Q36. (conceptual) Name three GoF patterns that JavaScript gives you "for free" via language features. Explain which feature provides each pattern.

Q37. (conceptual) A codebase has a file called utils.js with 200 exported functions. What anti-pattern is this? How would you refactor it?

Q38. (code) Write a simple Observer pattern (event emitter) from scratch. It should support on, off, and emit. Then show a practical use case (e.g., order placed notifies email, inventory, and analytics).


9.2.d — Writing Extensible Code (Q39–Q46)

Q39. (conceptual) What does "extensible code" mean in practice? Give an example of extensible vs non-extensible code.

Q40. (code) Design a plugin system for an Express application. Define the plugin interface, a plugin manager, and write two sample plugins (authentication and request logging).

Q41. (conceptual) When does extensibility become over-engineering? How do you decide where to add extension points?

Q42. (code) Implement a feature flag system that supports:

  • Global on/off toggle
  • Percentage-based rollout
  • User-specific overrides Show how a route handler uses the feature flag to choose between old and new behavior.

Q43. (code) Build a "notification engine" using the Strategy pattern where:

  • Each notification channel (email, SMS, push) is a strategy
  • Channels are registered at startup
  • New channels can be added without modifying the engine
  • The engine selects channels based on user preferences

Q44. (conceptual) Compare constructor injection, setter injection, and interface injection. When would you use each?

Q45. (code) Create a composition root (manual DI) for a small Express app with these components: Database, UserRepository, UserService, UserController. Show both the production wiring and the test wiring (with mocks).

Q46. (conceptual) Your team is building a data pipeline that currently reads from PostgreSQL and writes to S3. The product roadmap mentions MySQL and GCS as future sources/destinations. Do you build the abstraction now or wait? Justify your answer with reference to OCP, YAGNI, and the cost of extensibility.


Bonus Challenge Questions (Q47–Q50)

Q47. (design) You're building a food delivery app's order system. Apply at least four design principles (from SOLID and beyond) to design the architecture. Show the interfaces, classes, and how they interact. Explain which principles you applied and where.

Q48. (refactoring) Given this 80-line "god function," identify every principle violation and refactor step by step:

async function processCheckout(cart, user, paymentInfo) {
  // validate cart
  if (cart.items.length === 0) throw new Error('Empty cart');
  for (const item of cart.items) {
    const stock = await db.query('SELECT quantity FROM inventory WHERE product_id = $1', [item.id]);
    if (stock.rows[0].quantity < item.quantity) throw new Error(`${item.name} out of stock`);
  }
  // calculate totals
  let subtotal = 0;
  for (const item of cart.items) { subtotal += item.price * item.quantity; }
  const tax = subtotal * 0.08;
  const shipping = subtotal > 50 ? 0 : 9.99;
  const total = subtotal + tax + shipping;
  // apply discount
  if (user.loyalty === 'gold') { total *= 0.9; }
  if (user.loyalty === 'platinum') { total *= 0.85; }
  // charge payment
  const charge = await stripe.charges.create({ amount: total * 100, currency: 'usd', source: paymentInfo.token });
  // save order
  const order = await db.query('INSERT INTO orders ...', [user.id, total, charge.id]);
  // update inventory
  for (const item of cart.items) {
    await db.query('UPDATE inventory SET quantity = quantity - $1 WHERE product_id = $2', [item.quantity, item.id]);
  }
  // send notifications
  await mailer.send(user.email, 'Order Confirmation', `Your order #${order.id}...`);
  if (total > 100) await slack.send('#big-orders', `Big order: $${total} from ${user.name}`);
  return { orderId: order.id, total, chargeId: charge.id };
}

Q49. (debate) "Design principles and patterns are just job-security theater. Good developers write working code; patterns make it slower and harder to understand." Write a response that acknowledges the grain of truth in this statement while defending the value of principles.

Q50. (conceptual) Create a "Design Principles Checklist" — a list of 10 questions you would ask during a code review to check whether a piece of code follows good design principles.


Return to Overview