Software architecture is part of my job. And in my experience I can honestly say that if you would ask several people to design a system, they would all come up with something different. As such, it’s often very subjective. Of course there are objective arguments for choosing this or that, but quite often they come with a flavor of personal taste.
Anyway, my point is that in the course of previous years the ideas I had about architecting systems have evolved. As a matter a fact, if I look back at some of the designs I did, I would probably do it completely different nowadays. Because with experience comes maturity, and of course, also technology evolves. I’m confident that the ideas I have now will change again in the future, and in fact this is exactly what makes my job fun to do. Just always remember that there is no such thing as a perfect architectural design that is a solution for every problem.
The goal of this series is to show you an example how you could design a system. It’s kind of a reference architecture that I like to use (I have used it – a number of times in middle-sized projects, and I’m still quite happy about it), but it’s up to you to decide if you find some ideas to be usable in your specific environment. In no way I claim that these ideas are the one and only truth, instead, I would like to take this opportunity to learn from your comments and maybe change ideas based on feedback and new insights. So, this will always be a work in progress.
In this series…
Currently I posted 12 parts in this series:
Part 1: introduction, layers, onion architecture
Part 2: separating the commands from the queries, not so strict CQS
Part 3: a simple scenario, visual studio solution and projects, the domain layer
Part 4: the application layer
Part 5: the infrastructure layer (data access)
Part 6: the service layer
Part 7: the infrastructure layer (tests)
Part 8: the query side
Part 9: throwing business faults
Part 10: validation
Part 11: logging
Part 12: conclusion, where is the source code?
According to me, the design of a system should be as simple as possible (but not too simple), yet adaptable and extendable when needed. It means: don’t create anything that you might need later, only what you really need now, but be sure everything is in place so that you can relatively easy create it later if you need it (scale out). In other words, don’t make everything hopelessly complex just because you can and it looks nice, but choose wisely because there’s a valid reason for it.
In order to build a scalable and flexible system, it’s a good idea to group components into logical layers, define the relation between them as well as their dependencies. In general, we can define five layers.
The presentation layer consists of everything that is visible to the consumer of the system, in other words, everything that has a UI. This is a thin layer, so it delegates all work to the service layer, and is only dependent on that layer. Typically it uses service operations that have been designed in the most optimal way to be used by the UI.
The service layer holds everything that is needed to expose functionality to other systems. Typically exists of service contracts that define the interface of the exposed service operations, data transfer objects that define the requests and responses for those operations and service adapters that glue the layer to the application layer. It takes care of serialization of data, defining communication protocols and other configuration needed for interaction with other systems. This layer has dependencies on the application and domain layers.
The application layer is a thin layer responsible for application workflow. It orchestrates the work by using the domain layer and calls to external components, and defines transaction scopes in which they should be executed. This is also a good place to implement security and high-level logging. The logic in this layer can be exposed by the service layer in an asynchronous or synchronous way, or can be logic that is triggered by events. It has only a dependency on the domain layer.
This is the business heart that models the system and where the domain logic is implemented. Typically it contains domain entities, domain services and repository interfaces, all used by the application layer to execute domain logic. It has no dependencies to other layers.
This layer contains everything that is related to infrastructure, so this is the layer that is the most coupled to technological decisions: data access logic needed to talk to databases (concrete implementations of the repository interfaces defined in the domain layer), external libraries and providers, logging frameworks, file system logic and so on. This is the outer layer which is dependent on all the other layers.
To define relations between the layers, you probably know the traditional layered architecture that exists of UI on top, then business and then infrastructure layer, and where each layer talks to the layer directly under it. However, the problem is that in this representation the business layer is coupled to infrastructural concerns.
So instead of the traditional layered approach, I’m a big fan of the onion architecture to define dependencies between the layers of the system and to determine the coupling between them. Representing layers as an onion means that every layer can only depend on layers more central: coupling is done towards the center:
This architecture is an exact match to visualize the dependencies of the various layers that we defined; and goes hand in hand with inversion of control principle: implementation details are injected by the outer layers at runtime; the inner layers just define and use interfaces. As an example, the domain layer defines the repository interfaces it needs in order to be able to perform the work; but the concrete implementation is coded and injected by the infrastructure layer.
Next time I will continue on this, and talk a little about one of the key principles: separating queries and commands.