I’m getting through Expert F#, and was starting to consider how a domain model might be implemented. But I think before that it is important to consider where that model fits into the solution, which requires us to think about architecture.
I must say that my disorganized thoughts were put into a much clearer perspective by the F# for fun and profit post Organizing modules in a project and you will likely find that a more practical and clearer explanation than I’m about to make.
Architecture in general
Any solution beyond a few hundred lines of code needs an architecture to organize the code. An architecture provides hard rules and soft guidelines on how different parts of a solution should interact, with the primary goal of making the solution maintainable. A maintainable solution is one that is understandable, and therefore easier to work on and more secure; is slower to accumulate debt; and is more testable and therefore of higher quality.
An architecture creates organization by separating unrelated concerns, controlling message paths, and hiding information. The goal of this project is to create an API for an asset management solution so we are implementing a web service, and the most common organization for a web service architecture is to split the solution into layers. The previous post, ASP.NET Core with F#, briefly considered an implementation for the top layer – the web interface. Beyond that we must also consider:
- domain – the business rules and (for an API) the core business value;
- persistence – how data is stored and retrieved;
- infrastructure – cross-cutting concerns such as user context, configuration, logging.
In the most naive implementation, this creates the following architecture, where each layer depends on the one above:
I believe an architecture should be designed from the domain outwards because the domain is where the core business value resides. The domain comprises groups of data, which I will refer to as data objects, and domain operations which create or act upon the data. In an OO domain model the relevant operations are stored with the relevant data objects, although this tends to get messy.
Coming from C#, F# adds a new constraint: that types must be declared before they are used. This is not as different as it first seems since a domain focused solution (even in an OO stack) should define its data and behavior separate from its implementation to allow for unit tests (not to mention all the other benefits of clear architectural layers). In C# this means defining domain dependencies as interfaces, then injecting the concrete implementations once they are defined.
As F# is strongly typed, in order to have unit tests we must be able to provide at least two different objects (the concrete and a mock) that share a type. The end result is identical to C#: we must create interfaces to express behavior.
So the domain operations depend on:
1. data objects;
2. interfaces defining operations/services that must be provided. Some of those behaviors will be other domain operations and for the sake of consistency it might be desirable to include all the domain operation interfaces in this dependency.
In defining operations/services interfaces, the domain layer has essentially created the persistence and infrastructure interfaces.
With the domain nicely isolated, the remainder of the architecture must now provide what it needs. We expect that the concrete classes for these dependencies will be largely in the persistence and a little in the infrastructure. This results in a network as follows, with the addition of a startup layer which creates key context classes, such as the dependency injector.
At this point nothing here is F# specific – and that is a good thing! A good architecture should allow implementation decisions to be made as late as practical.
The end result is we now have a place for our domain data objects and operations, and we can begin to define those.