Have you ever noticed that most software architecture guidance divides the code in at least 3 distinct parts?
That's because there is a generic guiding principle being conveyed by all of them, a principle I call 'IO to the boundary'.
Hexagonal Design, Clean Architecture, Imperative Shell and even Model View Controller all try to tell you to push IO operations to the beginning and end of an operation, while doing no IO in the middle of the operation.
Pseudo Code
This implies that all this guidance should lead to the same top level code at any request entry point.
// first perform IO to load data
// then execute a core pattern which performs no IO
// finally perform IO to store data
Example MVC controller
As an examplen a top level entry point for a web request (using an MVC controller) likely should look something like this, regardless of the guidance you are following.
[HttpPost("{bookingId}")]
public async Task<IActionResult> Book([FromRoute] string bookingId, [FromBody] PlacePurchaseOrder command)
{
// perform IO to load data
var booking = await repository.Get(bookingId);
// invoke functional core, no IO
booking.PlacePurchaseOrder(command.PurchaseOrder, command.Name);
// perform IO to store data
await repository.Flush();
return Ok();
}
Benefits
When you stick to this simple rule a lot of problems in your system (regarding productivity, maintenance, performance and testability) will simply disappear:
- The overall code is easy to understand from a high level.
- The business logic is encapsulated in a core pattern and is less likely to be spread out across the code base.
- It's easy to test this business logic in isolation through unit testing.
- Dependencies are enforced to be encapsulated and can be mocked for testing purposes.
- Easier to ensure read or write performance as the respective operations are called from a well known location.
- It's simpler to maintain atomicity of the operation, as any outbound IO is located at the end.