Skip to main content
Back to engineering notes

Engineering note

Making dependency boundaries explicit

Application-owned contracts and explicit dependency direction.

Architecture / Dependency boundaries

Dependency ownership

Architectural boundaries depend on source references and responsibility allocation. Business rules, application orchestration and external integration should have explicit owners, supported by the dependency structure.

The projects provide two concrete separations: reusable framing shared by client connectors and server listeners, and upstream token strategies separated from caching and renewal coordination.

Proposed CLEAN structure

This proposed structure organises a modular gateway around domain rules, application operations and infrastructure adapters. It illustrates dependency direction rather than the projects' implemented assembly layout.

Source dependencies point towards the use case

  1. Presentation An API endpoint or operator action invokes an application operation.
  2. Application The use case coordinates the operation and owns the ports it requires.
  3. Domain Message rules and invariants remain independent of transport and storage frameworks.
Infrastructure implements application-owned ports. The composition root supplies those adapters. At runtime the use case can call an adapter, while the adapter's source dependency points back to the contract owned by the application.

Application-owned port

The independent C# example below defines a dispatch contract owned by the application layer.

using System;
using System.Threading;
using System.Threading.Tasks;

/// <summary>Application-owned dispatch boundary for an approved message.</summary>
public interface IApprovedMessageSender
{
    /// <summary>
    /// Sends an approved payload, completing when the adapter accepts the send.
    /// Cancellation or transport failure is reported by the returned task.
    /// Completion does not establish delivery to the final recipient.
    /// </summary>
    Task SendAsync(ReadOnlyMemory<byte> payload, CancellationToken cancellationToken);
}

A TCP adapter implements the port without exposing socket types to the use case. A test double can capture payloads or report failure. Task completion denotes acceptance by the adapter, not end-to-end delivery acknowledgement.

Dependency enforcement

In this proposed layout, application references domain, while adapters reference the contracts they implement. Compilation checks available references; a dependency check or architecture test is required to prevent prohibited project references being introduced.

Boundary verification should establish that use cases can be tested without a web host and that transport changes remain within the adapter. Framework types exposed through application contracts can undermine that separation.

Ports are appropriate at external dependencies that require application-level control. Separate representations are justified where their responsibilities or lifecycles differ.

Design restraint

A component should have a defined responsibility and clear ownership of state, failures and cleanup. A forwarding wrapper without an additional contract or policy introduces maintenance cost without necessarily improving separation.

Shared components require shared responsibilities. Superficial similarity alone is insufficient justification for coupling otherwise independent features through a common package.

Domain rules, application operations and transport adapters can remain within one process. Separate deployment requires an operational reason, such as independent scaling or ownership, and provision for the resulting failure and recovery behaviour.

Persistence and recovery requirements determine the appropriate mechanism. In-memory channels, message brokers and local file stores provide different guarantees and require verification against the intended use.

Examples are generalised or synthetic and omit operational details and identifying information.

← All engineering notes