API Design | Jan 12, 2024 | 9 min read
Explore the fusion of Minimal APIs with Vertical Slice Architecture in this blog. We'll compare it with traditional layered approaches, introduce 'slices' in software design, and guide you through implementing this innovative, modular method.
In a previous blog I've talked about structuring Minimal API.
One of the differences I mentioned was “better fit for vertical slice architecture”.
So today I want to explain to you a few things:
But before we dive into the “new way” of creating APIs, let's take a brief look at the traditional approach.
The traditional layered approach in software architecture refers to a design method that organizes a system's components or modules into distinct layers, each responsible for specific functionalities.
The most notable architectures utilizing this approach are:
Clean architecture
Here, each layer communicates with the adjacent layers following specific guidelines.
Often adhering to principles like separation of concerns and single responsibility, where each layer focuses on its designated tasks without tightly coupling with other layers.
But, this approach has a few challenges.
As with everything in the world of software development, benefits are often followed by challenges.
Layered architecture introduces:
Now, aware of these challenges, consider how you structure a feature.
Do you progress from “top to bottom,” intersecting the layers?
If so, congratulations, you are now thinking in slices.
Vertical Slice Architecture (VSA) is a software development approach that organizes applications into distinct functional slices.
The fundamental concept behind this architecture involves grouping code based on business functionalities and consolidating related code.
This approach yields several benefits:
Vertical slice architecture
Before we dive into the implementation part, we need to:
Aspect | Vertical slice architecture | Layer architecture |
---|---|---|
Scope | Focuses on end-to-end feature development for a single vertical or user story. | Divides the application into horizontal layers (e.g., presentation, business logic, data access) |
Organization | Organized around features/modules. | Organized around functional layers |
Dependency management | Reduces inter-module dependencies by encapsulating complete feature sets. | Tends to have interdependencies between layers. |
Flexibility | Easier to modify or replace specific features/modules. | Changing one layer may impact other layers. |
Testing | Encourages comprehensive testing of specific features/modules. | Testing often involves mocking layers for isolated tests. |
Initial setup | Often quicker to set up as it involves a smaller initial scope by focusing on a specific feature. | May require more upfront planning and architecture design to establish the structure of various layers. |
Complexity | Helps in managing complexity by focusing on cohesive feature development, potentially reducing the complexity within each vertical slice. | May lead to increased complexity within individual layers but promotes separation of concerns. |
Maintenance | Easier to maintain individual features/modules. | Maintenance can be complex due to interconnected layers. |
As you can see from the table, both approaches have their merits, and the choice ultimately depends on achieving the right balance between maintainability, flexibility, and scalability.
A "slice" denotes a self-contained development unit within an application, encompassing all necessary layers to implement a particular feature.
Every slice operates independently and strives to deliver a fully functional segment of the application capable of functioning autonomously.
Visual representation of slice
And now…the implementation part!
For the sake of simplicity, we are going to rely on data from previous post.
This is the project structure:
Project structure
I predominantly employ the Features folder approach for code organization.
This method mandates structuring slices within a single folder as extensively as feasible.
Features can be defined in two ways:
Moreover, this approach necessitates the implementation of the CQRS pattern since each slice is divided into sub-slices, or endpoints.
Organization types
Personally, I favor deep organization due to its advantages in maintainability and ease of navigation.
However, feel free to utilize the approach that best suits your preferences.
Now that we've finalized the project structure, let's direct our attention to defining the contents of each individual slice.
Each slice functions as a separate, independent unit, akin to a thread. In other words, a slice can be agnostic to the underlying libraries it employs.
Type of different slices inside one application
One slice can use EF Core, second raw SQL, and third can use stored procedure. Use whatever fits your needs to achieve the desired result.
All our slices will follow the REPR pattern.
It delineates web API endpoints with three primary components:
The solution is simple.
By consolidating the request, endpoint, and response within a slice, you minimize the time spent navigating through layers, thereby enhancing the maintainability and ease of working with endpoints.
Simply put, these two serve as Data Transfer Objects (DTOs), exclusively designed for data transfer purposes. Achieving immutability would be highly advantageous.
Fortunately, records align perfectly with this requirement.
We'll be implementing two endpoints:
The folder structures will resemble the following:
Folder structure
For achieving full immutability, we are going to use positional records:
public class Contracts
{
public record Response(string FirstName,
string LastName,
DateOnly BirthDate,
bool isActive);
}
public class Contracts
{
public record Response(string FirstName,
string LastName,
DateOnly BirthDate,
bool isActive,
string Address,
string PhoneNumber);
public record Request(int Id);
}
In both scenarios, we utilize a root class named Contracts.
While employing positional records, I typically opt to consolidate them within the same file to expedite navigation and ensure easier maintainability.
However, the choice of organizing records per file is also feasible.
Regarding the contract, our task is complete.
Let's proceed to the endpoint.
For the endpoint handler, we'll employ a class segregated into two parts:
public static class Endpoint
{
private static List<User> GetUsers() =>
Collection.Users
.Where(user => user.IsActive)
.ToList();
public static void AddEndpoint(this IEndpointRouteBuilder app)
{
app.MapGet("api/v1/users", () =>
{
var activeUsers = GetActiveUsers();
return Results.Ok(activeUsers);
});
}
}
public static class Endpoint
{
private static Response AddUser(Request request)
{
Collections.Users.Add(request);
return new Response(request.Id);
}
public static void AddEndpoint(this IEndpointRouteBuilder app)
{
app.MapPost("api/v1/users", (Request request) =>
{
var response = AddUser(request);
return Results.Ok(response);
});
}
}
I employ a practice known as mouse wheel-driven development, emphasizing the consolidation of all related data within a single file.
However, this approach might conflict with the Single Responsibility Principle (SRP), posing a tradeoff between expedited and centralized development and the adherence to SRP for better code organization.
It the end, add endpoint to the pipeline:
var app = builder.Build();
GetUsers.Endpoint.AddEndpoint(app);
AddUser.Endpoint.AddEndpoint(app);
While this approach seems suitable for managing a small number of features, scaling it to handle 50+ slices could pose challenges.
In such scenarios, I recommend considering specialized libraries:
Scaling to a larger number of slices might lead to various code smells, such as:
Shared code:
Cross-Cutting Concerns:
Poorly Defined Slice Boundaries:
The integration of Minimal API alongside vertical slice architecture represents a transformative paradigm in software development, providing an efficient means to design and deploy APIs.
Emphasizing simplicity, clarity, and modularity, this architectural approach streamlines the development process while elevating the maintainability and scalability of applications.
In conclusion, here are two crucial considerations, akin to "gold nuggets," to ponder before implementation:
Shadow APIs are invisible threats lurking in your infrastructure—undocumented, unmanaged, and often unsecured. This article explores what they are, why they’re risky, how they emerge, and how to detect and prevent them before they cause damage.
APIs are the backbone of modern software, but speed, reliability, and efficiency do not happen by accident. This guide explains what API performance really means, which metrics matter, and how to optimize at every layer to meet the standards top platforms set.
MCP servers are the backbone of intelligent, context-aware AI applications. In this guide, you’ll learn what sets the best ones apart, explore practical use cases, and get tips for building and deploying your own high-performance MCP server.