Back to basics - Code architecture
The structure of your code, the choice that could make or break your application before you've written a single character. Also known as code architecture. Here's an attempt at what we need it for, what it actually is and then an example on how you could set it up.
What do we want code architecture for?
What we want, of course, is 'good software'. But that's rather badly defined and not very helpful. Proper definitions like ISO 25010 tell us that there are a lot of properties that your architecture will have to deal with. Properties that you can't all satisfy perfectly. So you'll have to pick and choose which you want to be good, and which you can leave as good enough. For your typical enterprise application (which is what I have in mind for this blog) we want your code to be easy to change, while being fast enough. Note that this might not fit as well for things like a grep alternative or a fast paced game, so you'll have to make different trade-offs there.
What is code architecture?
In order to achieve all this we have only one tool available: walls. Walls and their gates. Also known as 'layers' or 'splitting things up'. It's determining what goes where. Walls allow you to separate bits of code from each other, so that they can't interfere as much anymore with one another.
When classes do need to communicate across a wall, they need to go through its gate, which
is what decides the rules on how to do that. It's these rules that make architectures
unique. For example, if you only allow classes on different sides of a wall to communicate
using events, you get event-driven architecture.
Or if you say that a request can either return a value or have a side effect, but not both,
you get CQRS.
In this blog we'll only consider two rules: communication through a gate can
only be one directional
or there can't be any communication at all.
These are very simple rules and for most applications that's all you need. After all, unless
you really need complexity, simpler is better.
Placing walls is a good thing, because it ensures that you can change one part of your code without having to worry about the other parts. It also makes your code easier to read, as you don't have to understand everything all at once. There are negative consequences too though. That's because if two bits of code can't interact anymore, then things cannot be optimized between them. So while we want to place walls to reach all those nice properties like maintainability, portability etc., we do want to do that by placing as few walls as possible.
As a rule of thumb: when things are related to one another, they should not have any walls between them. If they aren't, they should have a wall between them.
The approach of Domain-driven design
As an example we'll design a web API by borrowing ideas from Domain-driven design. We'll first look at the outer layers of the application. These contain all the 'boilerplare logic': Unwrapping our HTTP requests, getting our data (to and) from our database, stuff like that.
We'll put all the HTTP stuff behind a wall and call it the controller layer. If we need
things like message queues and events, we'll chuck these in that layer as well. We'll put
all the database stuff behind a wall and shove them in repositories.
If necessary we'll have application services to tie them together.
Now we have our request and all necessary data, and we can enter our domain layer. Inside
which we'll do all validation, decisions and computations. The result will come back in the
application layer again and we'll apply the database changes, queue the events and send out
our HTTP response.
Now let's look further inside where we find the two domain layers. This is where all the domain logic happens (and nothing else).
The innermost layer is where we'll find the domain model. Most of the logic rests here. It mainly consists of two kinds of objects: objects with an identity (called entities) and objects that are defined by their values (called value objects). Entities are usually the important objects that do all the logic, while value objects tend to be used to chuck some logic in to not clutter the entity too much. Groups of entities and value objects that work together (called aggregates) are walled off pretty strictly. While all objects inside an aggregate can communicate as they like, they are not allowed to know about objects outside the aggregate, and all communication from the outside has to go through a single entity (called the aggregate root). This aggregate is considered to be a unit, and is responsible to always keep itself consistent and valid.
Sometimes several aggregates need to work together, despite the walls prohibiting them from talking to each other directly. This is why we have domain services. Domain services are stateless classes, one layer above the domain model, that are responsible for directing the communication to the aggregates and to keep constraints across multiple aggregates valid.
Closing remarks
To wrap up we've seen that code architecture consists of limiting which classes can communicate with one other using what I call walls. Placing walls in strategic places helps your code stay organised.
In my experience setting up an application like this is well worth it. You do not, of course, have to place your walls the same way I do here in every application. But if you want to create an application that's capable of growth while still staying flexible, you've got to think about which bits go where and place your walls accordingly.
Related blogposts
- Domain-driven design - A draughts example app; illustrate the theory from this blogpost by creating a draughts webapp.