// orders/order.service.ts
import { SesClient } from "../infra/ses-client";
export class OrderService {
private ses = new SesClient();
async place(order: Order) {
// ...
await this.ses.sendEmail(order.email, "Order placed");
}
}
DIP (the Dependency Inversion Principle) says:
- high-level modules should not depend on low-level modules
- both should depend on abstractions (interfaces)
- abstractions should not depend on details
A high-level module holds business policy. A low-level module holds infrastructure details, like an email client.
In this example, the business policy (OrderService) imports an infrastructure detail (SesClient). So the source-code dependency follows the flow of control: from policy to detail.
To invert it:
1. The orders module declares what it needs: interface OrderNotifier { orderPlaced(order: Order): Promise<void> }.
2. An infrastructure class, SesOrderNotifier, implements that interface.
3. Now the infrastructure module imports from the orders module. This is the opposite direction to the runtime call.
Two things get inverted: the source-code dependency, and the ownership of the interface. The abstraction belongs to the high-level client. It is shaped by the client's needs, not by the SES API.
The benefit: you can swap SES for another provider, or for a fake in tests, without touching the policy code.
- High-level policy and low-level details both depend on an abstraction.
- The source-code dependency points against the flow of control.
- The high-level module owns the interface, and the interface is shaped for its needs.
- Details become swappable, including fakes in tests.