Policy above detail
Business flow should describe intent first, then delegate details through stable contracts.
DIP is often explained as “depend on abstractions, not concretions.” That is useful, but incomplete. The real intent is architectural: high-level policy should define the shape of the collaboration, while low-level detail should plug into it. In other words, the core business flow should not be authored around vendor APIs, transport concerns, or framework mechanics.
In order systems, the use case for placing an order should describe inventory reservation, payment authorization, and confirmation notification in business terms. It should not be structured around the internals of one payment gateway or one messaging client. DIP keeps the core decision-making layer in control.
DIP becomes most powerful when you think in layers. High-level policies define the purpose of the system. Low-level details implement storage, messaging, integration, and transport. If high-level code depends directly on low-level APIs, the dependency direction is backwards from the design intent.
In Java and Spring systems, dependency injection often makes DIP easier to implement, but DI is not the same thing as DIP. You can inject a concrete detail into a use case and still violate the principle if the business flow is authored around that detail.
The strongest DIP designs let the use case speak the language of the domain while details adapt themselves to that language.
Business flow should describe intent first, then delegate details through stable contracts.
The important abstraction is often shaped by the use case, not by the vendor API.
This principle helps packages and modules, not just classes and constructors.
package org.javaomnibus.ecommerce.principles;
public final class ConfirmPaymentUseCase {
private final PaymentStatusGateway paymentStatusGateway;
private final OrderRepository orderRepository;
public ConfirmPaymentUseCase(PaymentStatusGateway paymentStatusGateway, OrderRepository orderRepository) {
this.paymentStatusGateway = paymentStatusGateway;
this.orderRepository = orderRepository;
}
public void confirm(String orderId) {
PaymentStatus paymentStatus = paymentStatusGateway.statusFor(orderId);
if (paymentStatus == PaymentStatus.APPROVED) {
orderRepository.markPaid(orderId);
}
}
}
interface PaymentStatusGateway {
PaymentStatus statusFor(String orderId);
}
interface OrderRepository {
void markPaid(String orderId);
}
enum PaymentStatus { APPROVED, DECLINED }
DIP can introduce extra abstractions, and those abstractions should earn their existence. If a dependency is simple, stable, and unlikely to vary, a direct dependency may be acceptable. The principle is about dependency direction where it matters most.