Skip to content

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:

public class ArriveAtWarehouseEndpoint : Endpoint<ArriveAtWarehouseRequest>
{
public override async Task HandleAsync(ArriveAtWarehouseRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.ArriveAtWarehouse;
await context.SaveChangesAsync();
}
}
public record ArriveAtWarehouseRequest(Guid DeliveryId);
public class PickedUpByDriverEndpoint : Endpoint<PickedUpByDriverRequest>
{
public override async Task HandleAsync(PickedUpByDriverRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.PickedUpByDriver;
await context.SaveChangesAsync();
}
}
public record PickedUpByDriverRequest(Guid DeliveryId);
public class ShippedToCustomerEndpoint : Endpoint<ShippedToCustomerRequest>
{
public override async Task HandleAsync(ShippedToCustomerRequest req, CancellationToken ct)
{
var delivery = await context.Delivery.First(x => x.Id == req.DeliveryId);
delivery.Status = DeliveryStatus.ShippedToCustomer;
await context.SaveChangesAsync();
}
}
public record ShippedToCustomerRequest(Guid DeliveryId);

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 the TruckId or the TrackingId, but only for this case.

  • And how did it ever happen that this delivery went from ShippedToCustomer directly to ArriveAtWarehouse?
    That should never be possible, but some frontend just called SetDeliveryStatus 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.