Project Philosophy
The general mentality when using vertical slice architecture is all about thinking in endpoints.
Do not think in CRUD. Do not think in resources. Do not think in entities.
Think in features. Think in use cases. Think in endpoints.
Coupling and DRY
Coupling and DRY are always in direct competition and coupling is a WAY bigger problem than “duplicated” code.
Let me show you an example to illustrate a point about coupling and about DRY.
The most extreme case is when you have some kind of “going from status 1 to status 2 to status 3” type of logic.
Let’s say a we are tracking a delivery. It goes through different statuses.
InWarehouse => OnTheRoad => ShippedToCustomer.
I would create 3 endpoints. ArriveAtWarehouse. PickedUpByDriver. ShippedToCustomer.
All 3 endpoints would look like this:
You feel like an idiot writing these.
Obviously you could just use a UpdateDelivery
or SetDeliveryStatus
endpoint and give it any status you want.
-
But then it turns out when a delivery moves to
ShippedToCustomer
, we send a delivery arrived email. -
And when a delivery goes to
ArriveAtWarehouse
we mark them in some other system. -
And when a delivery goes to
OnTheRoad
we hook that delivery to that trucks GPS tracking.
Oh how do we do that? We need theTruckId
or theTrackingId
, but only for this case. -
And how did it ever happen that this delivery went from
ShippedToCustomer
directly toArriveAtWarehouse
?
That should never be possible, but some frontend just calledSetDeliveryStatus
with a DeliveryId and a DeliveryStatus.
When this situation comes up in a UpdateDelivery
endpoint or SetDeliveryStatus
endpoint, you keep adding conditionals, keep adding request parameters that only need to be set for certain status changes and just generally drown in complexity.
When this situation comes up in a vertical slice architecture, nothing happens. You don’t even notice it.
You add the email to an endpoint, no issues. You add the extra request parameter to another endpoint, no issues.
That is the beauty of this architecture and the reason I love it. It’s incredibly extensible and iterative. You don’t even notice the giant headache you just dodged.
It takes restraint and discipline to not go for the premature abstraction. It’s also hard to realize that you only got a problem, because you chose the wrong abstraction.
Who the hell questions an UpdateDelivery
endpoint? Who questions the SetDeliveryStatus
method?
Thats just DRY, right? Our system is just complicated, right? The requirements were just unclear, right?
Personally I started writing really repetitive, really dumb code to feel the pain.
I want to be forced into turning these 3 endpoints into one, because im spending soooooo much time writing and maintaining this messy duplicated code. I want to know where the breaking point really is.
Somehow I haven’t broken yet.
Abstraction is my enemy
- Don’t hide EF Core behind your repository.
- Don’t hide your service registration behind 2 other extension methods.
- Don’t hide sendgrids API behind your own INotifcationService interface.
Most people, including yourself, won’t question your abstraction. They will just use it.
If you don’t have an Any()
method in your repository, people will just use FirstOrDefault() == null
.
The odds of you getting it right are basically zero.
The odds of you creating a better database abstraction than Microsoft, while mainly building your AI marketing automation startup are literally zero.
I like for people to use the actual tools directly. Like SQL. Like EfCore. Like Fastendpoints.
Its easy to look up the documentation for how to do X on EfCore. But if something happens in your internal db abstraction, you get a call. Or people have to wade through your implementation.
If you really want to not use EfCore directly within endpoints, I would not write a classic repository.
I would think of it more like a service. A grouping of a bunch of methods that call EfCore queries and commands.
Don’t start abstracting over EfCore. Don’t add your own FirstOrDefault
that just wraps EfCores FirstOrDefault
.
Instead just go with DbCalls.GetDelivery()
or DbCalls.GetDeliveryForPageXY
. This way you don’t mess up your abstractions, but still have all your database code in one place.
This is usually done for organization and testing, but we are testing differently in this project.
I like going directly through the front door.
I would think of all pulled out code in this static utility way. If you really use the same code in 4 different endpoints, extract it.
But extract it into what is basically a static utilities class with a bunch of unrelated methods.
Don’t try to hard to give it a name. Don’t try too hard to give it a concept.
Its just procedural code that needs to be called in 7 endpoints.
I am not a crazy person. I understand in a real project you have to abstract and you will have concepts, abstractions and services in your application.
More Tips
Some less structured thoughts about my style. Why I do things the way I do. How I would suggest using this code base.
- Thinking in endpoints makes it easy to plan work or write tickets for someone else to implement.
Its simple to do upfront design, implementation and review when its just “takes in X, does Y, returns Z”. - Create endpoints for the frontend in whatever shape it needs. The backend serves the frontend.
Don’t let someone (yourself) make 3 http calls in javascript which map to 3 database calls and then transform the results into a new shape to display on screen.
Making database calls and transforming data is the backends job. - Never reuse Request types. If an endpoint takes in a very common DTO or entity, make that a property on your
RequestType
for that endpoint. - Rarely reuse Response types. I wouldn’t say never, but it should be rare.
- Don’t spend time thinking about the perfect abstraction or philosophical questions of where your code lives. Does it belong in the Service or the Repository or the Handler? Create an endpoint with a request and a response and just code.
- This setup, much like mediatr, allows some cool patterns for logging, validation and testing. Middleware / Decorator pattern kind of stuff.
- Almost all my reasoning comes back to “because it’s simpler”.
I find throwing different best practice acronyms at each other shuts off thinking. You hit a “ah but thats not DRY” wall and stop thinking there.
Final Thoughts
All of this is just my style and my philosophy. Nothing in this project stops you from deciding that all your logic is only allowed within Service
classes within the Domain
project.
Nothing stops you from putting your Request-, Endpoint and Response classes in 3 separate files.
I would heavily encourage you to try it though. Keep your code as unabstracted as possible for as long as possible.
Use EfCore
directly within endpoints.
Skip AutoMapper
and see when you actually go crazy from writing response.Name = entity.Name
.
Wait with the DeliveryHelper.SetDeliveryStatus(delivery)
implementation until you had to touch the endpoints 4 times, because of bug reports.
Wait with your BaseEndpoint
until you can’t take it anymore.
Write really stupid looking code until you actually feel the pain.
If your biggest problem in a coding project is that you feel stupid, because everything is so simple and repetitive.
You don’t have a problem.
Further resources
- FastEndpoints and its documentation.
Read through the entire thing from start to finish once. Its not that much and offers a lot of goodies. - Jimmy Bogard’s talk. Minute 2:53 to 11:48 should be mandatory viewing for every .net developer.
- CodeOpinion has a lot of great content about vertical slice architecture and in general.