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
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.
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 };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 };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
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.
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