We at Dolittle believe that properly crafted code will make for maintainable systems over time. Based on experience, we have found principles that helps us do just that and we’ve proven it time and time again that it truly does helps investing in this.
One of the hardest things to accomplish is consistency, even within a single codebase. The Dolittle frameworks and platform span a number of projects and repositories and it becomes increasingly more important to stay consistent. Consistent in structure, naming, approach, principles, mindset and all. The consistency enables a high level of predictability and makes it easier to navigate for anyone using Dolittle frameworks. For anyone maintaining Dolittle frameworks, it means that its easier to navigate and change context between tasks.
Rather than grouping artifacts by its technical nature; keep the things that are relevant to each other close. This makes it easier to navigate and provides a more consistent structure than having to divide by technical nature. For anyone coming into a project and developing on a specific feature will have an easier time understanding and mastering that feature when its all in the same location. Examples of division by technical nature would be keep all your interfaces in an interface folder/namespace, all your frontend components in a component folder. While what you’re trying to focus on is the feature and everything related to the feature.
High cohesion is core to the concept of a bounded context.
Divide only by the tier the artifacts belong to. See Example below.
+-- Bounded Context 1 | +-- Module 1 | +---- Feature 1 | | | View.html | | | ViewModel.js | | | Styles.css | | | SomeRestAPI.cs | | | SomeSignalRHub.cs | +---- Feature 2 | | | View.html | | | ViewModel.js | | | Styles.css | | | SomeRestAPI.cs | | | SomeSignalRHub.cs +-- Bounded Context 2 ...
+-- Bounded Context 1 | +-- Module 1 | +---- Feature 1 | | | Command.cs | | | CommandInputValidator.cs | | | CommandBusinessValidator.cs | | | CommandHandler.cs | | | SecurityDescriptor.cs | | | CommandHandler.cs | | | AggregateRoot.cs | | | Service.cs | +---- Feature 2 | | | Command.cs | | | CommandInputValidator.cs | | | CommandBusinessValidator.cs | | | CommandHandler.cs | | | SecurityDescriptor.cs | | | CommandHandler.cs | | | AggregateRoot.cs | | | Service.cs +-- Bounded Context 2 ...
+-- Bounded Context 1 | +-- Module 1 | +---- Feature 1 | | | Event.cs | +---- Feature 2 | | | Event.cs +-- Bounded Context 2 ...
+-- Bounded Context 1 | +-- Module 1 | +---- Feature 1 | | | ReadModel.cs | | | Query.cs | | | QueryValidator.cs | | | SecurityDescriptor.cs | | | AggregateRoot.cs | | | Service.cs | +---- Feature 2 | | | ReadModel.cs | | | Query.cs | | | QueryValidator.cs | | | SecurityDescriptor.cs | | | AggregateRoot.cs | | | Service.cs +-- Bounded Context 2 ...
Part of being able to move fast with precision is having a good automated test regime. One that runt fast and can be relied upon for avoiding regressions. Dolittle was built from day one with automated tests, or rather Specs - specifications. You can read more about how Dolittle does this here.
The SOLID principles aims to make it easier to create more maintainable software. It has been the core principles at play from the beginning of Dolittle. Below is a quick summary and some relations into Dolittle.
Every class should have a single responsibility, every method on this class should do only one thing. If it needs to do more things, it is most likely a coordinator and should delegate the actual responsibility to a dependency for the actual work. This is true for types and methods alike.
Systems and its entities should be open for extension, but closed for modification. A good examples of this is how you can extend your system quite easily by just putting in new event processor without having to change the internals of Dolittle.
Objects in a program should be replaced with instances of their subtypes without altering the correctness of that program. An example of how Dolittle follows this is for instance the event store works. It has multiple implementations and the contract promises what it can do, implementations need to adhere to the contract.
Interfaces should represent a single purpose, or concerns. A good example in .NET would be
IEnumerable concerns itself around being able to enumerate items, the
ICollection interface is about modifying
the collection by providing support for adding and removing. A concrete implementation of both is
Depend on abstractions, not upon the conrete implementations.
Rather than a system knowing about concrete types and taking also on the responsibility of the lifecycle of its dependencies.
We can quite easily define on a constructor level the dependencies it needs and let a consumer provide the dependencies.
This is often dealt with by introducing an IOC container into the system.
Dolittle is built around this principle and relies on all dependencies to be provided to it.
It also assumes one has a container in place, read more
Another part of breaking up the system is to identify and understand the different concerns and separate these out.
An example of this is in the frontend, take a view for instance. It consists of flow, styling and logic. All these are
Other good examples are validation, instead of putting the validation as attributes on a model in C# - separate these into their
own files like
Read more in details about it here.
At the heart of Dolittle sits the notion of decoupling. Making it possible to take a system and break it into small focused lego pieces that can be assembled together in any way one wants to. This is at the core of what is referred to as Microservices. The ability to break up the software into smaller more digestable components that makes our software in fact much easier to understand and maintain. When writing software in a decoupled manner, one gets the opportunity of composing it back together however one sees fit. You could compose it back in one application running inside a single process, or you could spread it across a cluster. It really is a deployment choice once the software is giving you this freedom. When it is broken up you get the benefit of scaling each individual piece on its own, rather than scaling the monolith equally across a number of machines. This gives a higher density, better resource utilization and ultimately better cost control. With all the principles mentioned in this article, one should be able to produce such a system and that is what Dolittle aims to help with.
Dolittle is heavily relying on different types of discovering mechanisms.
For the C# code the discovery is all about types. It relies on being able to discover concrete types, but also implementations of interfaces.
Through this it can find the things it needs. You can read more about the type discovery mechanism
It automatically knows about all the assemblies and the types in your system through the assembly discovery
done at startup.
Read more about conventions here.
When concerns are seperated out, some of these can be applied cross cuttingly. Aspect-oriented programming
is one way of applying these. Other ways could be more explicitly built into the code; something that Dolittle enables.
The point of this is to be able to cross-cuttingly enforce code. Things that typically are repetitive tasks that a developer needs
to remember to do are good candidates for this. It could also be more explicit like the
in Dolittle that enables one to declaratively set up authorization rules across namespaces for instance.
This type of thinking can enable a lot of productivity and makes the code base less errorprone to things that needs to be remembered,
it can be put in place one time and one can rely on it. Patterns like chain-of-responsibility
can help accomplishing this without going all in on AOP.
Null in code can be referred to the billion dollar mistake. You MUST at all times try to avoid using null. If you have something that is optional, don’t use null as a way to check for wether or not its provided. First of all, be explicit about what your dependencies are. A method should have overloads without the parameters that are optional. For implementations that are optional, provide a NullImplementation as the default instead. This makes program flow better and no need for dealing with exceptions such as the NullReferenceException
Exceptions should not be considered a way to do program flow. Exceptions should be treated as an exceptional state of the system often caused by faulty infrastructure. At times there are exceptions that are valid due to developers not using an API right. As long as it there is no way to recover an exception is fine. You should not throw an exception and let a caller of your API deal with the recovery of an exception. Exceptions MUST be considered unrecoverable.
Examples of naming of exceptions can be found in C# Coding Styles.
Mutability in code is a challenge. For instance when dealing with threading, if an object used between two different threads is mutable, you basically have zero chance of guaranteeing its state. By making it immutable and making it explicit that you create a new version of the object when mutating - you will avoid threading issues all together. This is very core to typical functional programming languages, but is a good mindset regardless of language.
Mutability however goes even further, methods should never return a mutable type - it should protect its internals and take
ownership of anything that can be mutated. That way you make your code very clear on responsibility. An example of this
in C# would be returning
IList<>from a method. Instead of returning this, you should be return en
List<> would be implementing
IEnumerable<>, so you don’t need to convert it to an immutable. This way the contract
is saying that you can’t control its mutation and the responsibility becomes very clear. This makes responsibilities and concerns