In Clean Architecture, Input Ports and Output Ports are concepts related to the separation of concerns between the core business logic (use cases) and the outside world (e.g., UI, databases, APIs). These ports establish boundaries that ensure the core logic remains independent of frameworks, delivery mechanisms, and external systems.
1. What Are Input Ports?
Definition:
- Input Ports define the use cases or the application’s behavior from the perspective of the core logic.
- They represent the entry points into the application layer and define what the system can do (e.g., “place an order” or “fetch a user”).
Role of Input Ports:
- Expose methods or interfaces that allow external systems (e.g., UI, controllers) to interact with the core application logic.
- Ensure that the outside world does not directly access or influence the core business logic.
Example:
- A use case interface acts as an input port.
public interface PlaceOrderInputPort {
void execute(PlaceOrderRequest request);
}
The use case implementation provides the actual logic.
public class PlaceOrderUseCase implements PlaceOrderInputPort {
private final OrderRepository orderRepository;
public PlaceOrderUseCase(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void execute(PlaceOrderRequest request) {
// Core business logic for placing an order
Order order = new Order(request.getItems());
orderRepository.save(order);
}
}
The controller (or UI) calls the input port to trigger the use case:
@RestController
public class OrderController {
private final PlaceOrderInputPort placeOrderUseCase;
public OrderController(PlaceOrderInputPort placeOrderUseCase) {
this.placeOrderUseCase = placeOrderUseCase;
}
@PostMapping("/orders")
public ResponseEntity<String> placeOrder(@RequestBody PlaceOrderRequest request) {
placeOrderUseCase.execute(request);
return ResponseEntity.ok("Order placed successfully");
}
}
2. What Are Output Ports?
Definition:
- Output Ports define the interfaces that the application layer uses to communicate with external systems, such as databases, APIs, or messaging services.
- They act as exit points from the application layer, ensuring the core logic does not directly depend on implementation details.
Role of Output Ports:
- Abstract external systems, allowing the core logic to remain agnostic of the specific technologies used.
- Provide flexibility to replace or change external systems without modifying the core application logic.
Example:
- An interface for a repository acts as an output port.
public interface OrderRepository {
void save(Order order);
Order findById(int id);
}
The implementation of the repository interacts with the database (or another external system):
public class SqlOrderRepository implements OrderRepository {
@Override
public void save(Order order) {
// SQL logic to save the order
}
@Override
public Order findById(int id) {
// SQL logic to retrieve the order
return new Order();
}
}
The use case depends on the output port:
public class PlaceOrderUseCase implements PlaceOrderInputPort {
private final OrderRepository orderRepository;
public PlaceOrderUseCase(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Override
public void execute(PlaceOrderRequest request) {
Order order = new Order(request.getItems());
orderRepository.save(order);
}
}
Key Benefits of Input and Output Ports
Input Ports:
- Decouple Core Logic from the UI:
- The core logic (use cases) doesn’t need to know whether it’s called by a web controller, a CLI, or a mobile app.
- Clear Use Case Definition:
- Input ports make it easy to identify what the system can do.
Output Ports:
- Abstract External Systems:
- The use case logic doesn’t need to know how data is persisted or fetched—it interacts through the output port.
- Flexibility:
- External systems (e.g., a database, a REST API) can be replaced or modified without changing the core logic.
- Testability:
- By mocking the output port, you can test the use case logic without relying on real external systems.
Practical Example
Input Port (Use Case Interface):
public interface RegisterUserInputPort {
void registerUser(RegisterUserRequest request);
}
Use Case Implementation:
public class RegisterUserUseCase implements RegisterUserInputPort {
private final UserRepository userRepository;
public RegisterUserUseCase(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public void registerUser(RegisterUserRequest request) {
User user = new User(request.getName(), request.getEmail());
userRepository.save(user);
}
}
Output Port (Repository Interface):
public interface UserRepository {
void save(User user);
User findByEmail(String email);
}
Controller:
@RestController
public class UserController {
private final RegisterUserInputPort registerUserUseCase;
public UserController(RegisterUserInputPort registerUserUseCase) {
this.registerUserUseCase = registerUserUseCase;
}
@PostMapping("/users")
public ResponseEntity<String> registerUser(@RequestBody RegisterUserRequest request) {
registerUserUseCase.registerUser(request);
return ResponseEntity.ok("User registered successfully");
}
}
Key Takeaways
- Input Ports:
- Define how the system’s core logic is accessed.
- Represent use case interfaces.
- Output Ports:
- Define how the system interacts with external systems.
- Represent abstract interfaces for external dependencies.
- Decoupling Layers:
- Input and output ports ensure the core application is independent of both the UI and external systems.
- Testability and Flexibility:
- Port abstraction allows easier testing and adaptability to change.