Often we build abstractions into our projects around technology layers i.e. web, business, domain, data, etc. However this shouldn’t be our primary concern, it’s a secondary concern. We should focus on functional units of work, and we might choose to split these functional units of work by their respective technology layers, however not all functional units of work need to use the same underlying architecture.
We want to primarily decouple features so that we’re able to scale in a way that can be supported by our organisational structure. Splitting by functional units of work supports growing from a single team managing a set of features to a team per feature.
What’s the best architecture to start with? Micro-service, onion, clean, hexagon architecture, n-tier? None of these. It’s best to start simple, a single project, with as good of a CI/CD process as you can achieve that offers quick feedback. i.e. a commit or the push of a container image triggers a deployment to a development environment.
Why Vertical Slice?
Vertical slicing allows your application and organisation to scale together quickly. A single team can own the entirety of the project in the beginning, and once reaching a peak in capacity can split along with the services of the application.
It’s much more efficient to work on files near each other. Let’s consider a scenario where we want to introduce a new field in the UI which we want to persist in the database. We’ll change the HTML to add the field, the clientside/server-side API models, the API controller, the service, and the entity/database table. Let’s look at the proximity of these changes in a horizontal vs vertical structure.
Api └─Services ├─ServiceA-Api <-- Change ├─ServiceB-Api └─ServiceC-Api Business └─Services ├─ServiceA <-- Change └─ServiceB Data └─Entities ├─ServiceA │ ├─EntityA <-- Change │ ├─EntityB │ └─EntityB └─ServiceB ├─EntityC ├─EntityD └─EntityE
FeatureA ├─Api <-- Change ├─ServiceA <-- Change ├─EntityA <-- Change ├─EntityB └─EntityC FeatureB ├─Api ├─ServiceB ├─EntityC ├─EntityD └─EntityE
As you can see the changes are much more local to each other in a vertical architecture.
This is an instant productivity boost, it’s far easier to navigate this project structure. It’s clear where you need to go to make a change. If you’re working on FeatureA you go to the FeatureA folder, and it’s given clear logical boundaries that will help us scale.
Scaling it up
As the project grows, both in team size and code base, you’ll want to begin thinking about the development workflow. Maybe Team A is responsible for FeatureA, and Team B is responsible for Features B and C.
Vertical slicing will help this transition as you can simply take the entirety of a feature and transition it into a new project which is independently deployable.
How do I know what constitutes a feature?
Look for cohesion, features should change together, if you’re often finding you’re having to make changes across multiple features it might be an indicator that you have the wrong split.
Identifying feature boundaries can be challenging especially in the beginning, which is why I recommend a single project as opposed to multiple projects, or worse starting with micro-services.
Iterate and re-evaluate boundaries, it’s fine to merge features or to split a feature into multiple features.
- Keep feature slices small and focused, each feature should only contain the necessary code to implement that feature
- Features should be modular, each feature should be thought of as a standalone module
- Features should have minimal dependencies on other features, consider messaging architectures, or service discovery to avoid direct dependencies
- Plan for scalability, the architecture should be designed to scale horizontally
- If there’s common behavior between features it’s fine to introduce a shared Common folder or a common project. An example might be that you want to implement logging in a standard way or caching etc.
- Avoid foreign key constraints between features, and use unique identifiers as a means to build a relation
- Ideally create a database, or schema per feature, this will help if you scale to implement microservices
I recommend subscribing to Derek’s channel, he posts a lot of interesting videos on architecture and design patterns.