Clean Architecture
Crossing Boundaries
Calling outward without a source-code dependency pointing outward — the trick behind the rule.
Every use case in this notebook has called outward — for data, from a repository. The Dependency Rule drew that crossing as a single arrow that bends back on itself: control passes outward, but the source-code dependency still points inward. Making that bend real, in a language with no other trick for it, means dependency inversion.
The handler that needs the outside world
The Dependency Rule showed OrderService publishing an OrderCreatedEvent after saving an order. OrderCreatedHandler is what receives it, and it lives in ApplicationCore — the same project as Order, the same project as IRepository<T>:
public class OrderCreatedHandler(ILogger<OrderCreatedHandler> logger, IEmailSender emailSender)
: INotificationHandler<OrderCreatedEvent>The handler lives in ApplicationCore and takes IEmailSender — a port this project owns, not an SMTP client.
{
public async ValueTask Handle(OrderCreatedEvent domainEvent, CancellationToken cancellationToken)
{
logger.LogInformation("Order #{orderId} placed: ", domainEvent.Order.Id);
await emailSender.SendEmailAsync("to@test.com",
"Order Created",
$"Order with id {domainEvent.Order.Id} was created.");Sending mail is a side effect of the order existing. The business rule already happened; this is the bent arrow calling out.
}
}
Confirming an order by email is not a business rule; it is a side effect that happens to run outside the process, over SMTP or some provider’s API. ApplicationCore is still not allowed to reference either one, so it references IEmailSender instead — an interface it declares for itself:
public interface IEmailSender
{
Task SendEmailAsync(string email, string subject, string message);
}ApplicationCore owns this port so SMTP, a provider SDK, and even a log-only fake stay outside the centre.
The bend in the arrow
LoggerEmailSender, in Infrastructure, is what actually runs when the handler calls SendEmailAsync:
public class LoggerEmailSender(IAppLogger<LoggerEmailSender> logger) : IEmailSender
{
public Task SendEmailAsync(string email, string subject, string message)
{
logger.LogInformation("to: {email}, subject: {emailSubject}, message: {emailMessage}", email, subject, message);
return Task.CompletedTask;
}
}Infrastructure implements the port. Swap this for a real SMTP client and OrderCreatedHandler does not change a character.
At runtime, control flows from OrderCreatedHandler outward into LoggerEmailSender, a class ApplicationCore has never heard of. At compile time, the source-code dependency runs the other way: Infrastructure references ApplicationCore to implement its interface, and ApplicationCore references nothing in Infrastructure at all. That is the whole trick behind the bent arrow — a use case calls outward by calling an interface it owns, and whatever sits on the other side of that interface is somebody else’s problem to wire up, the same way Frameworks & Drivers wires up every repository.
Swap LoggerEmailSender for a real SMTP client and OrderCreatedHandler does not change one character. Neither does a test that swaps in a fake. That is Screaming Architecture’s starting premise: a codebase built this way announces its business, not its plumbing.