Principles
Architecture Principles
The following principles are either embedded in the code design, or checked using architecture tests.
Modular Monolith
A modular monolith structures the application into independent modules with well-defined boundaries, split based on logical boundaries. Modules are loosely coupled and communicate through a public API.
The application exposes modules located in the src/Feat/ folder.
Architecture rule: Domain project should not reference other domain projects.
π What Is a Modular Monolith? β Milan JovanoviΔ
Clean Architecture

π Why Clean Architecture Is Great For Complex Projects β Milan JovanoviΔ
The architecture maps to Clean Architecture layers:
Presentation
src/UI/Server/
Application
src/Feat/Xxx/Workflows/
Domain
src/Core/Domain.Types/and src/Feat/Xxx/Model/
Infrastructure
src/Feat/Xxx/Data/
Hexagonal Architecture
The Clean Architecture is de facto compatible with the Hexagonal Architecture: the hexagon surrounds the Application and Domain layers.
The Hexagonal Architecture uses another terminology: dependencies are abstracted behind ports, implemented in outer layers by adapters. There are no prescribed ways to define ports and adapters: ports are not necessarily interfaces, and adapters are not necessarily referring to the Adapter design pattern (refactoring.guru, Wikipedia).
It distinguishes dependencies that drive the hexagon (left side) from those driven by it (right side).

Left side: The UI/Server project drives domains through their I{Domain}Api (ports) and adapts them to the Remoting API. Tests can exercise the I{Domain}Api, mocking its dependencies.
Right side: The Data/Infrastructure layer, with two levels of "ports and adapters":
Application Workflows define their right ports as Instructions. The Workflow Runner acts as the adapter, driving the Data Pipelines.
Data Pipelines can expose their dependencies as interfaces (
IXxxApi), implemented by concrete Clients, following the dependency inversion principle.
Vertical Slice Architecture
Instead of organizing your code by technical layers (
Controllers,Services,Repositories), Vertical Slice Architecture organizes it by business features. Each feature becomes a self-contained "slice" that includes everything needed for that specific functionality. π Vertical Slice Architecture Is Easier Than You Think β Milan JovanoviΔ
The Safe Clean Architecture does not implement vertical slices by the book, but applies its principle at the module level of the modular monolith: the domain projects in src/Feat/ are self-contained, including almost all layers: Application, Domain, Infrastructure β Workflows/ and Data/ folders in the code.
Screaming Architecture
Build a system that truly "screams" about the problems it solves, [β¦] that communicates its purpose through its structure. By organizing your system around use cases, you align your codebase with the core business domain. π Screaming Architecture β Milan JovanoviΔ
The Safe Clean Architecture applies this principle at two levels:
Domain projects can contain a Workflows/ folder where each workflow (use case) is in a dedicated file.
Architecture rule: Workflows should be in their dedicated file, named without the Workflow suffix.
Remoting API folders contain a handler per file, exposing the capabilities consumed by the Client.
Architecture rule: Remoting API request handlers should be sealed and in their dedicated file.
Design Principles
The following design principles support the architecture principles at a lower level. Their main purpose is to increase modularity by reducing coupling.
Abstractions
Abstraction hides implementation complexity (the "how") behind a simplified, essential interface (the "what"). In OOP, an interface is the most common form; in FP, it's a function type.
Benefits: Decoupling, Encapsulation, Stability, Testability, Transitivity cut (dependency firewall).
Warnings: Beware of leaky abstractions or abstractions at the wrong level. A bad abstraction costs more than no abstraction at all. More types involved means more indirections and potentially harder navigation.
Dependency Inversion (DIP)
This principle, abbreviated DIP, states that:
High-level modules should not import from low-level modules. Both should depend on abstractions.
Abstractions should not depend on details. Details should depend on abstractions.
It can be illustrated with the following diagram:
On the first line:
Adepends directly onB.Module Highdepends onModule Low, breaking the first statement of the DIP.
On the second line:
AandBboth depend onI:AdefinesIand wraps it,BimplementsI.The direction of dependencies is now inverted:
Module Lowdepends onModule High, through theIabstraction.At compile time,
Ais independent ofB, whereas at runtimeAdeals with an object whose real type isBin production code, or with a mock objectβa.k.a. test doubleβ in unit tests.
Benefits:
Abstractions. DIP shares the benefits and drawbacks of abstractions.
Inverting Control and Ownership. The true power of DIP lies in its ability to make high-level, policy-driving modules dictate the terms of engagement to low-level, detail-oriented modules. It goes far beyond simple swappability. True Plug-in Architecture is the most direct benefit, with interfaces acting as extension points. DIP is the primary mechanism for creating strong Architectural Boundaries. It ensures that dependencies always point inward, from "Details" toward the "Core Business Rules".
Architecture rule: Domain types should not depend on domain projects.
Architecture rule: Workflows should not depend on Data types. (Enforced by F# compilation order: Workflows/ is declared before Data/ in the .fsproj.)
Architecture rule: Domain projects should not reference the UI/Server project.
The abstraction between Workflows and Data are the program instructions β see Domain workflows.
Encapsulation
Limits direct access to internal state and behavior. Achieved mainly via private (inside projects) and internal (between projects) keywords.
Encapsulation in the domain projects:
UI/Servershould access only theI{Domain}Apiand theDependencyInjectionhelpers.Test projects can access internal members using
InternalsVisibleToentries in.fsprojfiles.
Architecture rule: Domain workflows should be internal.
Architecture rules for Data components:
Clients:
internalClient Settings:
public(needed for DI)Entities (DTOs):
public(to avoid serialization issues)Mappers:
internalPipelines:
internal
Architecture rule: Data Entities should not be used outside of their respective namespace.
Dependency Injection
DI achieves the Inversion of Control ("Don't call us, we call you!"). Dependencies appear in the type definition:
C# way: Constructor parameters β e.g.
UI/Server/Remoting/{Page}/{Request}Handlerdepends onFeatApi.F# way: Function parameters β e.g.
Feat/{Domain}/Data/{Api}/{Api}Pipelinedepends on{Api}Client(s).Program: Abstracted as instructions in the program-based domain workflows.
DI Container
The DI container handles object instantiation, life cycle (transient, scoped, singleton), and dependency graphs. Each layer is responsible for configuring the DI of its types, exposing IServiceCollection extension methods. The top-level method chains the lower-layer registrations.
Architecture Tests
The architecture rules listed above are enforced by tests located in tests/Shopfoo.Feat.Tests/ArchitectureTests.fs and tests/Shopfoo.Server.Tests/ArchitectureTests.fs:
Last updated