1. The Web Is Not the System
- The web is simply a delivery mechanism for the system’s functionality.
- The system’s core business rules and use cases should not depend on HTTP, URLs, or web technologies.
Core Idea:
- Treat the web as just one of many potential interfaces for interacting with the system (e.g., web, CLI, mobile app).
2. Core Logic Independence
- Core logic should not be tied to web-specific details like:
- HTTP requests or responses.
- URL routing.
- Session or cookie handling.
- Instead, the web layer acts as an adapter that translates HTTP data into a format usable by the core logic.
Example:
- Instead of directly using
HttpRequest
in a use case, pass an abstractedInput
object.
3. The Role of Controllers
- Controllers belong to the interface adapter layer and act as intermediaries between the web and the core application.
- Responsibilities of controllers:
- Parse incoming HTTP requests.
- Call appropriate use cases with translated inputs.
- Format use case outputs into HTTP responses.
Example:
@RestController
public class OrderController {
private final PlaceOrderUseCase placeOrderUseCase;
public OrderController(PlaceOrderUseCase placeOrderUseCase) {
this.placeOrderUseCase = placeOrderUseCase;
}
@PostMapping("/orders")
public ResponseEntity<String> placeOrder(@RequestBody OrderRequest request) {
placeOrderUseCase.execute(request.toDomainOrder());
return ResponseEntity.ok("Order placed successfully");
}
}
4. Use Cases Remain Web-Agnostic
- The use case layer contains application-specific workflows that are independent of the web.
- Example:
- A
PlaceOrderUseCase
should handle the business logic of placing an order without knowing whether it was triggered by a web request, a CLI command, or a scheduled job.
- A
Use Case Example:
public class PlaceOrderUseCase {
private final OrderRepository orderRepository;
public PlaceOrderUseCase(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void execute(Order order) {
if (order.isValid()) {
orderRepository.save(order);
} else {
throw new IllegalArgumentException("Invalid order");
}
}
}
5. Framework Independence
- Web frameworks (e.g., Spring, Django, Express) should not dictate the architecture of the system.
- Framework-specific elements should be confined to the outermost layer (e.g., controllers or route handlers).
- This ensures that the system can switch frameworks or delivery mechanisms with minimal changes.
6. Testing the Core Without the Web
- Since the core logic is independent of the web, you can test it without needing a web server.
- Controllers can be tested separately by mocking the use cases they interact with.
Example: Testing a Use Case
@Test
public void testPlaceOrderUseCase() {
OrderRepository mockRepository = Mockito.mock(OrderRepository.class);
PlaceOrderUseCase useCase = new PlaceOrderUseCase(mockRepository);
Order order = new Order(...);
useCase.execute(order);
Mockito.verify(mockRepository).save(order);
}
7. The Benefits of Treating the Web as a Detail
a. Flexibility:
- The same core logic can support multiple delivery mechanisms (e.g., web, mobile app, CLI).
b. Testability:
- Core logic can be tested in isolation, without needing a running web server or network.
c. Maintainability:
- Changes in web technology (e.g., migrating from Spring to Micronaut) don’t affect the core business logic.
d. Reusability:
- The core logic can be reused across different projects or interfaces.
Practical Example: E-Commerce System
1. Core Logic (Use Case):
- Business rules and workflows remain agnostic of the web.
public class PlaceOrderUseCase {
private final OrderRepository orderRepository;
public PlaceOrderUseCase(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void execute(Order order) {
orderRepository.save(order);
}
}
2. Web Layer (Controller):
- Controllers handle web-specific details and delegate to the use case.
2. Web Layer (Controller):
- Controllers handle web-specific details and delegate to the use case.
@RestController
public class OrderController {
private final PlaceOrderUseCase placeOrderUseCase;
public OrderController(PlaceOrderUseCase placeOrderUseCase) {
this.placeOrderUseCase = placeOrderUseCase;
}
@PostMapping("/orders")
public ResponseEntity<String> placeOrder(@RequestBody OrderRequest request) {
Order order = request.toDomainOrder();
placeOrderUseCase.execute(order);
return ResponseEntity.ok("Order placed successfully");
}
}
3. Testing Without the Web:
- The use case is tested independently of the web framework.
@Test
public void testPlaceOrder() {
OrderRepository repository = new InMemoryOrderRepository();
PlaceOrderUseCase useCase = new PlaceOrderUseCase(repository);
Order order = new Order();
useCase.execute(order);
assertNotNull(repository.findById(order.getId()));
}
Key Takeaways
- The Web Is a Delivery Mechanism:
- Treat the web as a detail, not the foundation of your architecture.
- Core Logic Should Be Web-Agnostic:
- Use cases and business rules should not depend on HTTP, URLs, or web technologies.
- Controllers Translate Data:
- Controllers handle the web-specific logic, delegating business workflows to use cases.
- Framework Independence:
- Keep frameworks confined to the outermost layer to enable flexibility and maintainability.
- Test Core Logic in Isolation:
- Decoupling the web layer allows for efficient and focused testing of business logic.