Sitemap
iamprovidence

iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence

Vertical Slice Architecture

10 min readMay 17, 2026

--

Press enter or click to view image in full size

It is common to build backend architectures around horizontal layers. We separate systems into familiar tiers like presentation, infrastructure, application, and domain, each responsible for a specific technical concern.

However, there is an alternative approach: vertical architecture. Instead of organizing code around technical concerns, the system is structured around features, with each vertical slice containing everything it needs — from API to persistence.

Let’s explore what vertical architecture is, the challenges you may face when adopting it, how to share code effectively, where to draw boundaries, and how features communicate with each other.

Problem with Traditional Layered Architecture

In a well-known layered architecture, when adding or modifying a feature, the change rarely stays in one place. Even a small functionality typically forces you to touch multiple layers at once:

  • you start by updating the Domain, changing entities, introducing new rules and constraints
  • then you add new tables, repositories, or DB objects, corresponding mappers in the Infrastructure layer
  • after that, you modify or create a use case, adjust validation logic, add DTOs, and more mapping code in the Application layer
  • finally, you expose everything through a controller or API endpoint in the Presentation layer

Instead of focusing on the feature itself, you end up coordinating changes across technical boundaries. This increases cognitive load and raises the risk of breaking unrelated parts of the system.

Layers are about managing coupling in a clear way, not cohesion.

What Vertical Slice Architecture Is

Vertical slice architecture takes a different approach.

Slices are about maximizing cohesion within a slice and minimizing coupling between them.

It builds on ideas from Screaming Architecture, but puts it further. Slices organized the code by feature, not by technical concern. This means that each slice contains everything it needs (API, use case, validation, business logic, persistence, infrastructure, etc).

So if you need to update functionality, all the changes will be concentrated within a single slice.

The benefits are:

  • changes are localized within a single slice
  • code navigation becomes simpler
  • folders remain small and focused, making them easier to manage and maintain
  • the risk of breaking unrelated functionality is reduced
  • features evolve independently
  • teams work in parallel
  • developers better understand the system's business capability
  • it’s easier to understand a module, its available use cases, the used infrastructure, etc
  • adding a new feature is easier, as you can identify similar behavior and replicate the structure
  • removing a feature is straightforward because you can simply delete the entire folder without affecting unrelated code
  • refactoring becomes easier; it is simpler to move, reorganize, or restructure functionality without impacting other parts of the system
  • extracting functionality from a folder into a separate project becomes easier, since related code is already grouped and self-contained
  • and so on and so forth…

You may be thinking: “Sounds like some kind of magic. So why isn’t everyone applying it everywhere? 🤔”.

Don’t push the horses. There are reasons 😅. The most common problems are:

  • slice granularity
  • code sharing
  • slices communication

Let’s address each problem one by one.

Slice Granularity

You might think, in theory, that you know where one slice ends and another begins. Buuuut… in practice, you’ll encounter many pitfalls before getting it right 😒.

While, for most people, grouping code by categories such as business logic or API endpoints is relatively simple to comprehend, organizing code within slices is more complex.

Defining boundaries is hard. Should you group around an entity or an aggregate? Around an action or a use case? Around a feature?

There is no one good solution.

Some claim that slicing by actions is wise, as you can use different tools for each action, but this is rarely practical and results in too thin slices.

Press enter or click to view image in full size

You can also group by entities, as it feels more intuitive. This way, you can combine all operations over a single entity into one slice. But business concepts often span multiple entities. Therefore, it’s usually better to organize code around aggregate boundaries.

Finally, and the way I recommend, is to design each slice around the entire feature. It is the broadest but also the most isolated concept.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

But what is even a feature 🤔?

A feature is a complete business capability that enables a user to achieve a specific goal. It represents a meaningful outcome, delivers measurable value, and can be understood, discussed, prioritized and used independently.

Such features could be course offering, student management, enrollment workflow, etc.

Press enter or click to view image in full size

A feature may span multiple aggregates, or it may be contained within just one endpoint, depending on the scope of the business capability it represents. You should define a slice around functionality that is meaningful to your business stakeholders, and not by the number of entities it includes. The key is that each slice fully encapsulates the feature it serves, keeping it cohesive, understandable, and easy to evolve.

Long story short, if your slices are too thin, you’ll likely end up with more shared code and duplication. On the other hand, slices that are too chunky risk turning into spaghetti code. This architecture demands stronger discipline, thorough code reviews, and constant boundary re-evaluation.

Code Sharing

In vertical slice architecture, your goal is to have independent, self-contained slices. Buuuut… it is easier said than done 😒.

Sometimes you need to share business invariants, rules, or even entities across multiple slices; other times, what needs to be shared is infrastructure, such as caching, persistence, or logging.

Therefore, you typically introduce layers above and/or below the slices. A common approach is to add the following supporting layers:

  • Core — define a foundation for your slices, containing part of the framework (composition root, UI pages layout, middlewares, etc)
  • Shared — shared infrastructure code, such as persistence, caching, logging, message bus, reused across multiple slices

Notice that even here we have dependency rules: references should flow from top to bottom, not the other way around, and never horizontally between slices.

For example, your Core module references each slice to execute an action triggered by the UI:

[ApiController]
[Route("api/course")]
public class CourseController : ControllerBase
{
[HttpPost("create")]
public IActionResult CreateCourse([FromServices] ICourseSliceApi sliceApi)
{
var result = sliceApi.CreateCourse();

return Ok(result);
}
}

On the other side, your slice can define the interface:

public interface ICourseDbContext
{
DbSet<Course> Courses { get; }
DbSet<Lesson> Modules { get; }
DbSet<Lesson> Lessons { get; }

Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

Where the implementation will be provided in the underneath shared layer:

public class AppDbContext : DbContext, 
ICourseDbContext, // 👈 course slice
IStudentDbContext, // 👈 students slice
...
{
public DbSet<Course> Courses { get; set; }
public DbSet<Module> Modules { get; set; }
public DbSet<Lesson> Lessons { get; set; }

public DbSet<Student> Students { get; set; }

public DbSet<Enrollment> Enrollments { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext));
}
}

This approach allows each interface to expose only the functionality a slice actually needs, which significantly reduces cognitive load.

The trade-off is that the shared layer ends up depending on all slices. This can become problematic: a change in one slice might propagate to the shared layer, potentially impacting other slices indirectly. To mitigate this, you can split the shared layer to limit the ripple effect and to avoid referencing unnecessary classes.

In practice, many teams keep all slices in a single project and separate them by folders; this keeps things simple and flexible. Moving each slice into its own project gives you stronger, compiler-enforced boundaries, but it also adds overhead and can be harder to maintain as the number of slices grows.

However, some people go even further, and keep all three layers (Core, Slices, Shared) in a single project😅.

This can work for small projects, but within a team quickly results in a mess. Nothing prevents breaking a dependency rule. There are no clear boundaries — no compilation errors or project reference restrictions — to enforce separation.

As a result, people coming from other architectures, like Hexagon, where the Core layer has a different meaning, tend to put domain rules, value objects, services, current user context, and basically anything that represents core concepts of the system there, making the code harder to maintain for everyone😒.

Your team should switch mindset and understand that this isn’t layered architecture: a slice’s business logic and infrastructure should remain within that slice, and shared functionality should go into the shared layer. However, it usually takes a long review cycle — some gentle scolding included — before the team fully gets it 😁.

Slices Communication

If you properly pick boundaries between slices, they would barely need to communicate. Buuuut… sometimes they do 😒.

If you’re familiar with similar architectures — where systems are organized around bounded contexts, like in a Modular Monolith or Microservices — you might feel confident and be tempted to implement a solution the same way, with slices interacting exclusively through a public API 🤓.

But those are slices, not modules. We don't need to overcomplicate stuff. To fetch data from another slice, you can rely on shared infrastructure and use the database tables you need.

public interface IStudentDbContext
{
// 👇 Access data owned by another slice in a read-only manner
IQueryable<Course> Courses { get; }

DbSet<Lesson> Students { get; }

Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}

If you not only need to fetch data, but also to perform an operation in another slice, instead of triggering an action, it’s better to take the reverse approach: publish an event via a mediator or an in-memory message bus (see github example), allowing the other slice to react to it.

For example, one slice publishes an event:

// Event definition
public record CoursePublishedEvent(Guid CourseId) : IEvent;

// Course slice publishes the event
public class CourseService(IEventBus _eventBus)
{
public void PublishCourse(Guid courseId)
{
. . .

// Notify other slices
_eventBus.Publish(new CoursePublishedEvent(courseId));
}
}

And another slice performs the callbacks.

// Students slice reacts to the event
public class NotifyStudentsHandler : IEventHandler<CoursePublishedEvent>
{
public void Handle(CoursePublishedEvent @event)
{
var students = _dbContext.Students.ToList();

foreach (var student in students)
{
_emailService.SendEmail(student, $"New Course is available: {@event.Course.Name}")
}
}
}

With the event-driven paradigm, you don’t have direct method calls, and this keeps slices decoupled while still enabling cross-feature reactions.

Summary

This architectural style goes by several names: Vertical Architecture, Slice Architecture, or Feature-Based Architecture.

Vertical Slice Architecture isn’t just about having neatly organized folders; it also defines the purpose of the Core and Shared layers and clarifies the dependencies between them, slices granularity, and how they communicate.

It’s a luck to see Vertical Slice applied in backend systems, though some projects — like IdentityService — adopt it successfully. It is more commonly used in frontend frameworks such as Angular or React, where concerns like tight coupling to the UI framework are less of an issue. In these environments, features don’t have complex domain logic and don't depend on external infrastructure dependencies, making slices easier to implement.

Press enter or click to view image in full size

While Slices offer clear benefits, they come with trade-offs that are important to understand:

  • it is designed to optimize for change rather than reuse
  • it emphasizes cohesion within a feature rather than minimizing coupling between components
  • this approach makes it harder to define feature boundaries
  • intentional code duplication is common, and reusing “shared” becomes more difficult
  • if a lot of code must be shared between slices, that’s usually a sign that the boundaries have been drawn incorrectly
  • core and shared layers have references across slices, making them harder to replace, as they are stable components
  • without high discipline in a team and physical boundaries, a project can turn into a “big ball of mud”, where responsibilities overlap, logic is duplicated, and features unnecessarily reference one another

That said, let’s hope I haven’t scared you too much, and have encouraged you just enough to give it a try 🙃. For those who get that far, as always a small joke:

Press enter or click to view image in full size

Don’t forget to give the article a rating if you liked it 👏
Let me know if you have faced this architecture in practice💬
You can support me using the link below ☕️
Share with friends 🗣 Slice, by slice 🤫
Check out my other articles ⬇️
And don’t forget to follow ✅
See Ya😉

--

--