HackIndia Logo
HackathonsNewsSkillsGrowth
HackIndia LogoContact: +91 9910125804

General

  • Companies
  • Colleges
  • Board of Advisors
  • Brand Ambassadors
  • FAQ
  • Contact
  • Report a Bug

Hackathons

  • Upcoming Hackathons
  • Judges
  • Mentors
  • Partners
  • Participating Colleges
  • Testimonials

Resources

  • About Us
  • Branding
  • Latest News
  • Growth
  • Skills
  • Builders
  • Ecosystem
  • Forum
  • Ideas
  • Prompt Optimizer
  • Games

© 2026 HackIndia. All rights reserved.

Privacy PolicyTerms of ServiceCode of ConductSitemap

Build 2.139 · 05 Nov 2026, 10:27 UTC

    HackIndia
    HackathonsNewsInternshipsSkillsGrowthPro Perks
    ← Refactor code with patterns

    All the reading

    Design Patterns

    8 lessons, about 38 minutes. The check's questions come from these.

    Contents
    Spotting refactor signals
    1. 1. Spot pattern-shaped problems
    2. 5. Use singleton with caution
    3. 8. Ship your pattern refactor repo
    Creating objects with factories
    1. 2. Create objects with factories
    Swapping behavior with patterns
    1. 3. Swap behavior with strategy
    2. 4. React to events with observer
    Boundaries and dependency wiring
    1. 6. Bridge boundaries with adapter and repository
    2. 7. Wire code using dependency injection

    Spotting refactor signals

    1. Spot pattern-shaped problems

    5 min

    In 30 seconds
    • Spot repeated code pain before naming a pattern.
    • Separate real refactor signals from normal small-app mess.
    • Create a short pattern candidate note for your repo.
    Key point

    The core move

    Do not start with pattern names. Start with pain in your code.

    A pattern is useful only when it removes repeated change, branching, coupling, or setup confusion. If the code is simple and stable, leave it simple.

    What to look for

    Pattern-shaped problems usually show up after you add the second or third feature. One hackathon screen is often too early. Three similar payment, login, or notification paths are better signals.

    Your job in this lesson is to mark candidates, not fix them yet. Later lessons will teach specific patterns.

    Your to-do

    Scan your code

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    5. Use singleton with caution

    4 min

    In 30 seconds
    • Use Singleton only for one shared process-wide object.
    • Refactor global state into a small, testable access point.
    • Explain why Singleton was useful, or why you rejected it.
    Key point

    One idea

    Singleton is for one object that must have exactly one shared instance in your app process. Use it rarely.

    It is not a shortcut for passing data around. If you use it for normal app state, tests and future changes become harder.

    When it fits

    A Singleton can fit for a logger, config reader, analytics client, or connection manager where creating many copies causes real pain.

    Ask one question first: would two instances break something or waste a shared resource? If not, you probably do not need Singleton.

    Common mistake

    Common misuse

    Do not make UserSession, Cart, CurrentOrder, or HackathonTeam a Singleton just because many files need it.

    8. Ship your pattern refactor repo

    5 min

    In 30 seconds
    • Package your refactor so a reviewer can inspect it fast.
    • Write a README that explains pain, pattern, and trade-off.
    • Submit proof with clear commits, tests, and one rejected pattern.
    Key point

    One review path

    Your repo should let a reviewer move from problem to refactor to proof in under five minutes.

    Do not just show neat final code. Show the messy pain, the pattern you used, and why the change helped.

    Your to-do

    Shape the repo

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Example

    Creating objects with factories

    2. Create objects with factories

    5 min

    In 30 seconds
    • Use a factory when object creation has rules, branches, or setup steps.
    • Refactor scattered new calls into one named creation point.
    • Show a before-and-after factory refactor in your repo.
    Key point

    One idea

    A factory is a function or class whose job is to create objects for the rest of your app.

    Use it when creation is becoming messy: if-else branches, default values, validation, config, or choosing between similar classes.

    Why this matters

    In a hackathon app, object creation often starts simple. Then you add UPI and card payments, admin and student users, test data and production config.

    If every route, screen, or controller decides how to create these objects, changes become risky. A factory puts that decision in one place.

    Common mistake

    Do not factory everything

    Swapping behavior with patterns

    3. Swap behavior with strategy

    5 min

    In 30 seconds
    • Replace messy if/else behavior with a Strategy interface.
    • Add new behavior without editing the main checkout flow.
    • Refactor one real feature and explain why Strategy helped.
    Key point

    One idea

    Use Strategy when one part of your code chooses between multiple ways to do the same job.

    The caller should not know the details. It should ask a strategy to do the work.

    Where it appears

    You see Strategy-shaped pain when a method has many branches based on type, mode, provider, plan, or payment method.

    Common examples: UPI vs card vs COD payments, percentage vs flat discounts, CSV vs PDF exports, or different scoring rules in a quiz app.

    Common mistake

    Do not pattern everything

    Do not create Strategy for two tiny if statements that never change. That adds files without removing real pain.

    Boundaries and dependency wiring

    6. Bridge boundaries with adapter and repository

    5 min

    In 30 seconds
    • Hide external API and database details behind small interfaces.
    • Use Adapter for mismatched outside code, Repository for data access.
    • Refactor one boundary so your app can test and change it safely.
    Key point

    Boundary rule

    Keep your app code speaking your app’s language. Put outside shapes, SDK calls, SQL, Firebase code, and payment responses at the edge.

    Adapter changes an external interface into one your app expects. Repository gives one place to read and write domain data.

    Why this matters

    Hackathon apps often start with API calls inside button handlers or routes. It works until you change from a mock API to Supabase, MongoDB, Firebase, or a college server.

    If the boundary is hidden, your core code stays stable. You can swap the outside part without rewriting the feature.

    Adapter versus repository

    Use an Adapter when some outside thing has the wrong shape. For example, a payment SDK returns amount in paise, but your app uses rupees.

    Use a Repository when code needs stored data. For example, getUser, saveProject, listSubmissions. The repository hides whether data came from SQL, JSON, localStorage, or an API.

    10 questions, 7 to pass.

    Sign in to take the test
    Example

    A real smell

    This is not bad code because it works. It is a candidate because every new provider changes the same function.

    Do not jump to a solution yet. Write down the pain first.

    function pay(provider, amount) {
      if (provider === "upi") {
        return sendUpiCollect(amount);
      }
      if (provider === "card") {
        return chargeCard(amount);
      }
      if (provider === "wallet") {
        return debitWallet(amount);
      }
      throw new Error("Unknown provider");
    }
    
    // Pain: adding netbanking means editing this function again.
    // Candidate: behavior changes by provider.
    Common mistake

    Name chasing

    A common beginner move is to say, “I will use Factory, Strategy, Observer, Singleton,” before reading the code. That creates extra classes and weak README explanations.

    Recruiters and maintainers care more about why you changed the design. Pattern names help only after the problem is clear.

    Tip

    Use AI carefully

    You can ask an AI tool to review one file for design smells. Paste the file and ask for repeated branching, repeated setup, and tight coupling only.

    Check the answer yourself. Reject suggestions that require big rewrites, paid services, or patterns that solve problems your app does not have.

    Prompt:
    Review this file for pattern-shaped refactor candidates.
    Only list:
    1. Repeated branching
    2. Repeated object setup
    3. Tight coupling to external services
    For each item, quote the exact lines and explain the change pain.
    Do not suggest a pattern name yet.
    Example

    Candidate note

    Start a small note in your repo. It becomes proof that you made design choices, not random rewrites.

    Keep each note short. One problem, one reason, one possible direction.

    refactor-notes.md
    
    Candidate 1: Payment provider branching
    File: src/payments/pay.js
    Pain: Adding a new provider means editing pay() and retesting all providers.
    Signal: Same if-else block chooses behavior by provider.
    Possible direction: Move provider-specific behavior out of this function.
    Decision later: Choose a pattern only after checking the rest of the app.
    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    That turns normal business data into hidden global state. A teammate changing it in one place can break another screen without an obvious function call.

    Example

    Refactor a logger

    Before: every service creates its own logger. Later you want one format and one file target, so changes spread everywhere.

    After: one Logger instance is exposed through a controlled static method. The constructor stays private.

    class Logger {
      private static instance: Logger | null = null;
    
      private constructor() {}
    
      static getInstance(): Logger {
        if (Logger.instance === null) {
          Logger.instance = new Logger();
        }
        return Logger.instance;
      }
    
      info(message: string) {
        console.log(`[INFO] ${new Date().toISOString()} ${message}`);
      }
    }
    
    // usage
    const logger = Logger.getInstance();
    logger.info('Payment request created');
    Tip

    Keep it small

    Make the Singleton boring. It should coordinate one shared service, not become a bag of helper methods.

    If you are using AI to refactor, prompt it with the exact pain: “Refactor only the logger into a Singleton. Do not change business logic.” Then check the diff for hidden global state.

    Your to-do

    Safe refactor path

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Common mistake

    Testing trap

    Singletons can make tests leak state. One test changes the instance, and the next test sees the old value.

    Prefer stateless Singletons. If the object stores data, add a clear reset method only for tests, or choose dependency injection later in this track.

    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    README skeleton

    Use the README as your viva answer. A recruiter should not need to guess why you used Factory, Strategy, Observer, Repository, DI, or skipped Singleton.

    # Pattern Refactor: Campus Expense Splitter
    
    ## What this app does
    Students can add shared expenses, split amounts, and view balances.
    
    ## How to run
    npm install
    npm test
    npm run dev
    
    ## Before refactor
    The first version had payment logic, storage calls, and notification code inside the same service.
    This made small changes risky.
    
    ## Refactors made
    
    | Pattern | Problem it solved | Main files | Commit |
    | --- | --- | --- | --- |
    | Factory | Creating UPI, cash, and card payments had repeated if-else code | src/payments/paymentFactory.js | abc123 |
    | Strategy | Split rules changed often: equal, percentage, exact amount | src/splits/strategies | def456 |
    | Observer | Balance updates needed to notify UI and audit log without coupling them | src/events | ghi789 |
    | Repository | Data access was mixed into business logic | src/repositories | jkl012 |
    | Dependency Injection | Tests needed fake repositories and fake notifiers | src/container.js | mno345 |
    
    ## Pattern I did not use
    I did not use Singleton for the database client.
    The app is small, and passing the dependency explicitly made tests easier.
    A Singleton would hide shared state and make test order bugs more likely.
    
    ## Proof
    - Tests pass with npm test
    - Screenshots are in /docs/screenshots
    - Before branch: before-refactor
    - Final branch: main
    Tip

    Use AI carefully

    Ask AI to review your README for missing proof, not to invent reasons. Paste your real file tree and commit list.

    Check every suggestion against your code. If the README says Strategy but your code still has one giant if-else, fix the code or remove the claim.

    Prompt:
    Review this README for a pattern refactor repo.
    Tell me what proof is missing.
    Do not add patterns I did not implement.
    Here is my file tree:
    [paste tree]
    Here are my commits:
    [paste commit messages]
    Common mistake

    Avoid pattern dumping

    Four patterns are required for this track proof, but more is not automatically better. A repo with six patterns and no clear problem looks weaker than a repo with four useful refactors.

    Also include one pattern you rejected. This shows judgement, which is the real skill.

    Your to-do

    Submission checklist

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    A factory is not needed when you only write new User(name) once and there are no rules around it.

    If the factory only hides one simple constructor, it adds noise. Your README should explain the pain it removed, not just name the pattern.

    Example

    Before: scattered creation

    Here is a small TypeScript example from a campus event app. Payment creation is duplicated across routes.

    class UpiPayment {
      constructor(public upiId: string) {}
      pay(amount: number) {
        return `Pay ₹${amount} using UPI ${this.upiId}`;
      }
    }
    
    class CardPayment {
      constructor(public last4: string) {}
      pay(amount: number) {
        return `Pay ₹${amount} using card ending ${this.last4}`;
      }
    }
    
    function registerForWorkshop(input: any) {
      let payment;
    
      if (input.method === "upi") {
        payment = new UpiPayment(input.upiId);
      } else if (input.method === "card") {
        payment = new CardPayment(input.last4);
      } else {
        throw new Error("Unsupported payment method");
      }
    
      return payment.pay(input.amount);
    }
    Example

    After: named factory

    Now the route asks for a payment object. It does not know the creation rules.

    type PaymentMethod = "upi" | "card";
    
    type PaymentInput = {
      method: PaymentMethod;
      upiId?: string;
      last4?: string;
    };
    
    class PaymentFactory {
      static create(input: PaymentInput) {
        if (input.method === "upi") {
          if (!input.upiId) throw new Error("UPI ID is required");
          return new UpiPayment(input.upiId);
        }
    
        if (input.method === "card") {
          if (!input.last4) throw new Error("Card last4 is required");
          return new CardPayment(input.last4);
        }
    
        throw new Error("Unsupported payment method");
      }
    }
    
    function registerForWorkshop(input: any) {
      const payment = PaymentFactory.create(input);
      return payment.pay(input.amount);
    }
    Your to-do

    Refactor safely

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Tip

    Use AI carefully

    Ask AI to find repeated object creation and suggest a factory. Then check the output yourself.

    Watch for over-engineering. If it adds abstract classes, interfaces, and folders for one simple object, ask for a smaller version.

    Prompt:
    Find places in this code where object creation is duplicated or has branching logic. Suggest one simple factory refactor. Keep the public behavior the same.
    
    Check:
    - Did it preserve existing inputs and outputs?
    - Did it move creation rules into one place?
    - Did it avoid extra layers that are not needed?
    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Use it when behavior changes often, branches are growing, or each branch has different rules and tests.

    Example

    Before refactor

    This checkout code works, but every new payment method edits the same function. That makes merge conflicts likely during a hackathon.

    It also mixes checkout flow with payment rules.

    // checkout.js
    function pay(order, method) {
      if (method === "upi") {
        console.log(`Collect ₹${order.total} using UPI for ${order.customerVpa}`);
        return { status: "paid", provider: "upi" };
      }
    
      if (method === "card") {
        console.log(`Charge ₹${order.total} to card ending ${order.cardLast4}`);
        return { status: "paid", provider: "card" };
      }
    
      if (method === "cod") {
        console.log(`Mark ₹${order.total} as cash on delivery`);
        return { status: "pending", provider: "cod" };
      }
    
      throw new Error("Unknown payment method");
    }
    Example

    After refactor

    Now each payment method is a strategy. The checkout flow only calls pay.

    Adding wallet payment means adding one new strategy and registering it. You do not edit checkout.

    // paymentStrategies.js
    const paymentStrategies = {
      upi: {
        pay(order) {
          console.log(`Collect ₹${order.total} using UPI for ${order.customerVpa}`);
          return { status: "paid", provider: "upi" };
        }
      },
    
      card: {
        pay(order) {
          console.log(`Charge ₹${order.total} to card ending ${order.cardLast4}`);
          return { status: "paid", provider: "card" };
        }
      },
    
      cod: {
        pay(order) {
          console.log(`Mark ₹${order.total} as cash on delivery`);
          return { status: "pending", provider: "cod" };
        }
      }
    };
    
    function getPaymentStrategy(method) {
      const strategy = paymentStrategies[method];
      if (!strategy) throw new Error("Unknown payment method");
      return strategy;
    }
    
    module.exports = { getPaymentStrategy };
    
    // checkout.js
    const { getPaymentStrategy } = require("./paymentStrategies");
    
    function pay(order, method) {
      const strategy = getPaymentStrategy(method);
      return strategy.pay(order);
    }
    
    module.exports = { pay };
    Your to-do

    How to refactor

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Tip

    Use AI well

    Ask AI to identify the repeated action and suggest Strategy names. Then check that every strategy has the same method signature and return shape.

    Do not accept a huge class hierarchy blindly. In JavaScript or Python, plain objects and functions are often enough.

    Prompt:
    I have this function with if/else branches. Refactor it using the Strategy pattern. Keep it simple. Use the same input and output shape. Explain what changed and what tests I should run.
    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    4. React to events with observer

    4 min

    In 30 seconds
    • Use Observer when one event must notify many parts of your app.
    • Refactor direct calls into subscribe, unsubscribe, and notify steps.
    • Show the before-and-after in your repo with a clear README note.

    Where it fits

    Use Observer when something happens in one place, and many other parts need to react.

    Example: an order is paid. You may need to send an email, update inventory, create a receipt, and notify an admin dashboard.

    Key point

    One idea

    The event source should not know every reaction. It should only publish an event to listeners that subscribed.

    This keeps one change, like adding WhatsApp alerts later, from touching your payment code.

    Common mistake

    Common wrong refactor

    Do not use Observer just because two files talk to each other. A direct function call is fine when there is only one clear receiver.

    Observer helps when the list of reactions changes often, or when you want to add and remove reactions without editing the event source.

    Example

    Before refactor

    Here the payment service knows every side effect. Every new reaction makes this method longer.

    class PaymentService {
      pay(order) {
        order.status = "PAID";
    
        EmailService.sendReceipt(order.userEmail, order.id);
        InventoryService.reduceStock(order.items);
        AdminNotifier.notify("Order paid: " + order.id);
      }
    }
    Example

    After refactor

    The payment service now publishes one event. Listeners handle their own work.

    This example is plain JavaScript, so you can copy it into a small Node or frontend project.

    class EventBus {
      constructor() {
        this.listeners = {};
      }
    
      subscribe(eventName, listener) {
        if (!this.listeners[eventName]) this.listeners[eventName] = [];
        this.listeners[eventName].push(listener);
      }
    
      unsubscribe(eventName, listener) {
        this.listeners[eventName] = (this.listeners[eventName] || [])
          .filter(item => item !== listener);
      }
    
      notify(eventName, data) {
        (this.listeners[eventName] || []).forEach(listener => listener(data));
      }
    }
    
    const eventBus = new EventBus();
    
    eventBus.subscribe("order.paid", order => {
      EmailService.sendReceipt(order.userEmail, order.id);
    });
    
    eventBus.subscribe("order.paid", order => {
      InventoryService.reduceStock(order.items);
    });
    
    eventBus.subscribe("order.paid", order => {
      AdminNotifier.notify("Order paid: " + order.id);
    });
    
    class PaymentService {
      pay(order) {
        order.status = "PAID";
        eventBus.notify("order.paid", order);
      }
    }
    Tip

    Check the boundary

    Name events after facts that already happened, not commands. Use order.paid, not send.email.

    This keeps listeners independent. One listener can send email, another can update stock, and the event name still makes sense.

    Your to-do

    Refactor safely

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Example

    Small TypeScript example

    Here the app wants a simple PaymentGateway. Razorpay or any other provider can have its own response shape behind an adapter.

    The app service does not know provider fields. It only knows pay().

    type PaymentResult = {
      ok: boolean;
      referenceId: string;
    };
    
    interface PaymentGateway {
      pay(amountRupees: number): Promise<PaymentResult>;
    }
    
    class RazorpayAdapter implements PaymentGateway {
      constructor(private razorpayClient: any) {}
    
      async pay(amountRupees: number): Promise<PaymentResult> {
        const response = await this.razorpayClient.createPayment({
          amount: amountRupees * 100
        });
    
        return {
          ok: response.status === "captured",
          referenceId: response.id
        };
      }
    }
    
    class CheckoutService {
      constructor(private payments: PaymentGateway) {}
    
      async checkout(totalRupees: number) {
        const result = await this.payments.pay(totalRupees);
        if (!result.ok) throw new Error("Payment failed");
        return result.referenceId;
      }
    }
    Example

    Repository shape

    A repository should sound like your product, not your database tables. Keep query details out of routes and UI components.

    Start with the methods your feature needs today. Do not build a full data layer for imaginary screens.

    type Project = {
      id: string;
      title: string;
      ownerId: string;
    };
    
    interface ProjectRepository {
      findByOwner(ownerId: string): Promise<Project[]>;
      save(project: Project): Promise<void>;
    }
    
    class ApiProjectRepository implements ProjectRepository {
      constructor(private baseUrl: string) {}
    
      async findByOwner(ownerId: string): Promise<Project[]> {
        const res = await fetch(`${this.baseUrl}/projects?ownerId=${ownerId}`);
        return res.json();
      }
    
      async save(project: Project): Promise<void> {
        await fetch(`${this.baseUrl}/projects`, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(project)
        });
      }
    }
    Common mistake

    Common mistake

    Do not create one giant repository called AppRepository. That becomes a dumping ground.

    Split by domain need: ProjectRepository, UserRepository, SubmissionRepository. If a method is only used by one screen and has no pain yet, plain code may be enough.

    Tip

    Use AI carefully

    Ask AI to find boundary leaks: “List places where API, SDK, SQL, or localStorage details are mixed into business logic. Suggest adapter or repository names.”

    Check the output. Reject wrappers that only rename one function and add no testability or swap benefit. Keep names close to your app domain.

    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    7. Wire code using dependency injection

    5 min

    In 30 seconds
    • Pass dependencies into a class instead of creating them inside it.
    • Refactor hard-coded services into small, testable wiring code.
    • Show DI clearly in your repo without adding a large framework.
    Key point

    One idea

    Dependency injection means an object receives what it needs from the outside. It does not create or search for those dependencies itself.

    Use it when a class is stuck to a real database, payment API, email sender, clock, or AI client, and you want to swap that dependency for tests or local demos.

    Where DI fits

    You already used adapter and repository to hide boundaries. DI is how you connect those boundaries to the rest of the app.

    Think of it as wiring. Your business code says, “I need a PaymentGateway.” The app startup decides whether that is Razorpay, a fake UPI gateway, or a test double.

    Common mistake

    Common mistake

    Do not add DI just to look senior. If a class only uses normal values like strings or numbers, passing them is enough.

    Also avoid a DI framework in a small hackathon repo unless your stack already uses one. Manual wiring in one file is easier for reviewers to understand.

    Example

    Before: hard-coded

    This checkout service creates its own gateway. Now every test touches the real gateway shape, and you cannot run a clean offline demo.

    class RazorpayGateway {
      async charge(amountRupees, userId) {
        // call payment provider here
        return { ok: true, paymentId: "pay_123" };
      }
    }
    
    class CheckoutService {
      constructor() {
        this.gateway = new RazorpayGateway();
      }
    
      async buyCourse(userId, amountRupees) {
        const result = await this.gateway.charge(amountRupees, userId);
        if (!result.ok) throw new Error("Payment failed");
        return result.paymentId;
      }
    }
    
    module.exports = { CheckoutService };
    Example

    After: injected

    Now CheckoutService depends on a role, not a concrete class. The app wires the real gateway. Tests can pass a fake.

    class CheckoutService {
      constructor(paymentGateway) {
        this.paymentGateway = paymentGateway;
      }
    
      async buyCourse(userId, amountRupees) {
        const result = await this.paymentGateway.charge(amountRupees, userId);
        if (!result.ok) throw new Error("Payment failed");
        return result.paymentId;
      }
    }
    
    class RazorpayGateway {
      async charge(amountRupees, userId) {
        return { ok: true, paymentId: "pay_123" };
      }
    }
    
    class FakePaymentGateway {
      async charge(amountRupees, userId) {
        return { ok: true, paymentId: "fake_payment" };
      }
    }
    
    // app.js: production wiring
    const checkout = new CheckoutService(new RazorpayGateway());
    
    // checkout.test.js: test wiring
    const testCheckout = new CheckoutService(new FakePaymentGateway());
    
    module.exports = { CheckoutService, RazorpayGateway, FakePaymentGateway };
    Your to-do

    Refactor safely

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done

    Tip

    Use AI carefully

    Ask an AI tool: “Refactor this class to use constructor dependency injection. Do not change behavior. Keep the wiring explicit.”

    Check that it did not rename public methods, hide wiring in global state, or add a DI container package you did not ask for.

    Your to-do

    Do this now

    Do each one yourself, then tap it to tick it off. The ticks are only a checklist for you: they are not marked or scored.

    0 of 5 done