PASTA Architecture


Introduction
Many modern software projects begin with well-intentioned layered designs, such as Clean Architecture and Onion Architecture, but often devolve into a tangled mess of “spaghetti” code. The PASTA architecture – which stands for Ports, Adapters, Slices, Typed Abstractions (Anti-layers) – offers a solution to these issues. PASTA combines concepts from Hexagonal Architecture, Vertical Slice Architecture (VSA), and the Functional Core/Imperative Shell pattern, distilling them into a lean, pragmatic style. In this article, we’ll first briefly examine traditional Clean/Onion architectures and their downsides, then explore how PASTA draws inspiration from earlier patterns. Finally, we’ll introduce PASTA’s concepts and walk through an example of its code in action, demonstrating how it keeps business logic modular, testable, and served al dente.
TLDR;
The PASTA architecture is an attempt to untangle the spaghetti of layered enterprise applications, allowing us to build software that is both robust and easy to understand. By centering on vertical feature slices, using typed function ports instead of bloated interfaces, and keeping the core logic pure, PASTA achieves many of the goals of Clean Architecture (independent, testable business logic) with a fraction of the ceremony. The example we walked through demonstrates how a real use-case can be implemented with very little code – focused on what the use-case should do, not infrastructural concerns.
PASTA is especially appealing for modern cloud and microservice applications where over-engineering a full layered architecture can slow down development. It favors convention over configuration: if you follow the philosophy of keeping logic where it belongs and depending on simple delegates for I/O, you naturally end up with a codebase that’s modular and testable. As with any approach, adopting PASTA should be done with judgment – some very large or regulatory-heavy systems might still warrant additional layering or formal boundaries. But for many projects, it provides a refreshing simplification without sacrificing good design principles.
In the end, the ethos of PASTA can be summarized by its manifesto quote: serve your architecture al dente – firm and well-structured, but never overcooked. By avoiding the traps of both spaghetti code and lasagna code, PASTA offers a tasty middle ground that developers can enjoy. Bon appétit, and happy coding with PASTA!
Follow GitHub repository for code examples, manifesto, and more.
The Trouble with Layered Architectures (Clean/Onion)
Clean Architecture (by Uncle Bob) and the related Onion Architecture are popular domain-centric designs that enforce layered separation (e.g. Domain, Application, Infrastructure layers). In principle, they aim for a system where the business logic is independent of frameworks, UI, or database – improving maintainability and testability. However, in practice the way developers often implement these architectures can lead to several problems:
Feature Code Scattered Across Layers: A single feature may require touching multiple projects/folders (UI, application service, domain model, repository), causing constant context-switching. Pieces of the same functionality end up far apart, reducing cohesion. For example, adding a simple feature might mean editing DTOs in one layer, validators in another, mappers in yet another, etc. This slows down development and makes the code harder to follow.
Anemic Domain & “Service Soup”: Many Clean/Onion projects inadvertently create an anemic domain model, where “Entities” are just data bags and all logic sits in services. You might have bloated service classes (Application Services, UseCase interactors, etc.) orchestrating everything, often just calling other services or repositories. This proliferation of shallow services – what one might call “service soup” – is precisely the kind of over-engineered layering that adds complexity without clear benefits. Business logic gets diluted across multiple layers of indirection.
Excessive Boilerplate and Indirection: The strict layering usually means defining lots of interfaces for every interaction (e.g. repositories, gateways) and wiring them via dependency injection. In simple applications, this can be overkill – you end up writing extra code (interfaces, DTOs, mappers) that does little except pass data between layers. As James Hickey bluntly puts it:
The usual way developers implement Clean Architecture is horrible
Often mirroring the same issues as traditional 3-layer (N-tier) architectures . It’s common to see trivial pass-through methods in one layer calling the next, adding little value.
Complex Project Structure: Splitting an application into many layer-specific modules/projects can complicate the build and deployment. Jeff Palermo (who coined Onion Architecture) notes that having separate compiled modules for each layer can make the build more complex and impose overhead in smaller projects. In a typical enterprise .NET solution, you might see projects named
MyApp.Domain
,MyApp.Application
,MyApp.Infrastructure
, etc. – a structure that’s conceptually neat but can become cumbersome for development (especially if every minor change forces you to touch multiple projects).
In short, layered architectures can become “overcooked spaghetti” if applied rigidly. You end up with tightly coupled layers (despite intentions), lots of indirection, and code that’s hard to navigate. These pain points have led architects to seek alternatives that preserve the core ideas of separation of concerns and testability, but with better cohesion and less ceremony.
For more detailed overview on above-mentioned problems follow my previous articles Stop Abusing Interfaces and (Over)Use Of Repositories.
Inspiration from Hexagonal, Vertical Slices, and FCIS
Before diving into PASTA itself, let’s look at the key architectural ideas it builds upon:
Hexagonal Architecture (Ports & Adapters): Also known as the Ports and Adapters pattern (from Alistair Cockburn), Hexagonal architecture preaches a separation between the core domain logic and the outside world. The domain is at the center, and other concerns (UIs, databases, external services) connect via abstract ports and adapter implementations. In practice, this often means defining interfaces in the domain layer for things like repositories or gateways, which are then implemented in the infrastructure layer. The result is that the core logic has no direct dependency on, say, a specific database – it only knows about an interface (port). This concept is foundational to Clean/Onion as well. However, PASTA takes a unique spin on “ports & adapters” by using function types (delegates) instead of traditional interfaces (more on that soon).
Vertical Slice Architecture (Feature Slices): Rather than organizing code primarily by technical layer, vertical slice architecture advocates organizing by feature or use-case. All the code for a given feature (UI, logic, data access) is grouped together, respecting logical boundaries but not forcing physical separation. The benefit is high cohesion – “things that change together live together” . Jimmy Bogard popularized this approach in the .NET community, discouraging generic layers in favor of feature folders or modules. For example, you might have a feature folder “Booking” containing everything related to booking a class: request/response models, the business logic, and any persistence code. This minimizes the jumping around and keeps related code close. PASTA wholeheartedly adopts this vertical slicing: each use-case is essentially its own slice containing all relevant logic.
Functional Core, Imperative Shell: This pattern, attributed to Gary Bernhardt’s talk “Boundaries”, says to structure software with a pure, functional core (all your business logic as deterministic functions with no side effects) and an imperative shell around it (responsible for I/O, state, and side-effects). The idea is that your core logic can be modeled as functions that take input data and return output (or a description of an action to perform), and the outer layer actually performs the effects (database writes, network calls, etc.). This leads to very testable core logic (since it’s just pure functions) and a thin outer layer gluing things together. PASTA draws on this principle by clearly separating pure domain logic (the “Core”) from side-effectful operations. In PASTA, the core logic (e.g. business rules, validations) is kept free of I/O, while the shell handles orchestrating those core functions and calling out to external systems. As an analogy, if typical layered architecture is like a lasagna (layers affecting each other in complex ways) , the functional core/imperative shell approach is more like making a pizza: the core ingredients and flavors stay on the pizza, and the oven (shell) just provides the heat. PASTA aims to keep the business logic “ingredients” clean and only use the shell for what’s necessary.
By combining these ideas – ports & adapters decoupling, vertical feature slices, and functional core separation – we get the recipe for PASTA. Now let’s see what PASTA specifically brings to the table.
Introducing the PASTA Architecture
Figure: PASTA architecture diagram. Each use-case “slice” encapsulates its own handler and output port functions, surrounding a pure Core (domain logic) in the center. Adapters (green and purple boxes) on the boundaries implement these ports to communicate with external systems (like databases or email), while the Shell represents the imperative outer layer handling I/O and orchestration.
PASTA is an architectural style (developed in the .NET ecosystem) that explicitly aims to avoid the over-engineering of strict layered architectures. In the words of its manifesto:
“Layered systems are like overcooked spaghetti — sticky, tangled, and a pain to digest. PASTA serves it al dente.”
At its core, PASTA is about vertical slices of functionality with minimal layering. The acronym stands for Ports, Adapters, Slices, Typed Abstractions, which captures the key elements:
Ports as Typed Functions: In PASTA, a “port” is just a function signature (delegate) that defines an operation the core needs, such as saving a record or fetching data . Interfaces are not sacred here – in fact, PASTA deliberately avoids creating lots of interface types for every interaction . Instead, you might have a delegate like
Task<Result<Class, BookingError>> GetClass(Guid id)
to retrieve a class by ID, orTask SaveBooking(Booking booking)
to persist a booking. These are plain function types (typed delegates) rather than interface methods. This choice eliminates much of the boilerplate “ceremony” of Clean Architecture (no need to defineIClassRepository
with a single method, etc.) and makes dependencies very lightweight. Adapters will implement these delegates by simply pointing to a function or lambda. In tests, you can supply a simple lambda or stub function for a port – no mocking framework needed .Adapters (Outbound) at the Edges: An adapter in PASTA is an implementation of a port that calls an external system or resource. For example, you might have a Database adapter that provides the function for
GetClass
(querying the DB) andSaveBooking
(inserting into the DB). Because ports in PASTA are just delegates, an adapter can be as simple as a static class with methods, or even just assigning a lambda. The key is that the core logic doesn’t depend on how the adapter works – it only knows the delegate to call. PASTA primarily focuses on outbound adapters (calls that the system makes to the outside), and in simple cases it foregoes explicit “inbound” adapters, because frameworks (like ASP.NET Core) already handle input routing. In the PASTA sample, the ASP.NET minimal API endpoint itself acts as the inbound adapter (HTTP transport), calling the handler function directly. This keeps things minimal.Vertical Slices (Features) instead of Layered Projects: PASTA organizes code by feature/use-case, not by technical layer. Each use-case is a self-contained slice that includes its handler (business logic orchestration) and any related port definitions or logic. There are no separate “Services” or layers to scatter the logic – “logic lives where it belongs.” In practice, you might have a folder like Features/Booking/CreateBooking containing a
CreateBookingHandler
and perhaps a file defining needed ports (e.g.IBookingRepository
in a traditional approach becomes a set of delegates likeGetMember
,GetClass
,SaveBooking
). This slice handles one specific application action. By doing this, PASTA avoids the common layered architecture issue where one module owns all “logic” and another owns all “data access” – which leads to that service soup. Instead, each slice encapsulates the logic and the data access contracts it needs. This is essentially applying the Vertical Slice Architecture pattern: high cohesion within a feature, low coupling between features.Typed Abstractions over Primitive Obsessions: The “Typed Abstractions” part of PASTA encourages using rich domain types and functional results instead of primitive types or opaque data. For example, instead of returning error codes or boolean statuses, a PASTA use-case returns a Result<T, E> type (in the sample, from the FunqTypes library) which either holds a result or a typed error. The sample defines a
BookingError
hierarchy of error cases (like ClassFull, AlreadyBooked, MemberNotFound, etc.) as records derived from an abstract BookingError. This acts like a discriminated union of error causes. By using these domain-specific types and results, the code is more self-documenting and safer. It’s also part of the functional core philosophy – favoring pure data in/out. PASTA also tends to use immutable records for data (as in C# 9+ records) to represent things like Booking, Member, etc., which makes the domain model simple and immutable by default . All these typed constructs help avoid the ambiguity and coupling that can come from using raw primitives or large interface-based frameworks.Minimal Framework/DI Impact: In PASTA, the dependency injection (DI) container is used in a very limited way – basically to register the actual adapter implementations or a factory for the delegates. It is not used to define the architecture or to inject dozens of tiny services everywhere. PASTA’s philosophy is that DI should wire up the pieces you’ve designed, not dictate the program structure. Thus, you won’t find heavy use of DI containers injecting interface abstractions at multiple layers. Instead, for example, you might register a
BookingDependencies
object as a singleton which holds all the necessary function implementations for booking-related ports (we’ll see this next). Then an API endpoint can simply receive thatBookingDependencies
and pass it to the handler. This is a much more straightforward approach than having the DI container construct a large object graph of interdependent services.
In summary, PASTA’s goal is simplicity with power: Keep each use-case’s logic together and focused, keep the core logic pure, define clear typed boundaries (ports) for side-effects, and avoid unnecessary layers and abstractions. PASTA explicitly took inspiration from Hexagonal, Vertical Slices, and Functional Core/Imperative Shell, but trimmed the fat. As the manifesto says, “fewer layers, more clarity” . Now, let’s step through an example to see how PASTA actually looks in code.
PASTA in Action: Example Project Overview
To illustrate how a PASTA-structured application comes together, let’s examine a simplified “PastaFit” sample (a gym class booking system) built with PASTA architecture. This system manages Classes, Members, and Bookings (members can book classes, cancel bookings, etc.). We will see how the code is organized into slices and how the data flows through the system:
Functional Core - Pure Data and Logic
In the sample, core domain types are simple C# records in the Core/Domain folder. For example, a Booking is just defined as:
public sealed record Booking(Guid Id, Guid MemberId, Guid ClassId, DateTime Time);
Likewise, Member and Class are records with basic properties (an IsActive
flag for Member
, a Capacity
for Class
) . These have no methods – just data. Any invariants or calculations would be done in functions elsewhere (to keep them pure). The domain also defines the possible errors as mentioned. For instance, BookingError
is an abstract record, and specific error cases like ClassFull
or AlreadyBooked
inherit from it with predefined messages . This approach provides a type-safe way to represent things that can go wrong when making a booking.
Ports (Contract) – Delegates for Operations
The “Features” layer of the project contains a BookingPorts
definition which declares all the needed operations as delegates. Instead of interfaces like IBookingRepository
, we have function signatures such as:
public delegate Task<Result<Member, BookingError>> GetMember(Guid memberId);
public delegate Task<Result<Class, BookingError>> GetClass(Guid classId);
public delegate Task SaveBooking(Core.Domain.Booking booking);
Each delegate corresponds to an action the application might need to perform outside the core. In this case, GetMember
and GetClass
will retrieve a member or class (possibly from a database), and SaveBooking
will persist a new booking. Notice they return either a result or void task – errors are encoded in the Result<,>
with BookingError if something goes wrong (e.g. member not found). By using delegates, we keep these as plain function types – they can easily point to any appropriate function at runtime.
Dependencies
These delegates are then grouped for convenience. The sample has a CreateBookingDependencies
and CancelBookingDependencies
records that bundles port delegates needed for the booking use-cases:
public sealed record CreateBookingDependencies(
HasExistingBooking HasExistingBooking,
IsClassFull IsClassFull,
GetMember GetMember,
GetClass GetClass,
SaveBooking SaveBooking,
GetBooking GetBooking
);
public sealed record CancelBookingDependencies(
GetBooking GetBooking,
Ports.CancelBooking CancelBooking
);
This is essentially a data structure holding function references. When we initialize the application, we’ll create an instances of CreateBookingDependencies
or CancelBookingDependencies
by providing implementations for each of these delegates. Then, when a use-case handler runs, it receives this dependencies object and has all the functions it needs (to call database, etc.) readily available. This explicit passing of dependencies makes it very clear what external actions a use-case might perform.
Adapters (Implementations) – In-Memory Example
For the sake of the example, the adapter is in-memory (no real database). There is an InMemoryBookingAdapter
class that contains in-memory collections (lists/dictionaries) for storing Booking, Class, and Member data . It also has methods to bootstrap some initial data (creating a few sample classes and members). Most importantly, it provides methods: GetCreateBookingDependencies()
and GetCancelBookingDependencies()
which returns a respective dependency object wired up with appropriate function implementations. For example, the adapter sets GetMember
to a function that looks up a member in the Members dictionary and returns a Result<Member, BookingError>
– either success with the member or a failure with MemberNotFound
error if not present. Similarly, SaveBooking
adds a new booking to the Bookings list, HasExistingBooking
checks the list to see if a given member already booked the class, IsClassFull
checks if the class’s current bookings reach its capacity, etc. Here’s a glimpse of how one of these is set up in the adapter:
// Inside InMemoryBookingAdapter.Bootstrap()
GetClass: classId =>
Task.FromResult(Classes.TryGetValue(classId, out var cls)
? Result<Class, BookingError>.Ok(cls)
: Result<Class, BookingError>.Fail(new BookingError.ClassNotFound())),
SaveBooking: booking =>
{
Bookings.Add(booking);
return Task.CompletedTask;
},
// ...other delegates...
In that snippet, GetClass
is implemented as a lambda that finds a class by ID in the in-memory list and returns an Ok or Fail result accordingly, and SaveBooking
simply adds the booking to the list. By constructing the CreateBookingDependencies
in this way, the adapter essentially declares “when the core needs to GetClass or SaveBooking, here’s what to do.” . In a real application, these would call a database or an external API, but the core logic wouldn’t know the difference – it just calls the delegate.
The adapter also registers itself with the dependency injection container. In the sample, during startup we do something like:
InMemoryBookingAdapter.Bootstrap();
services.AddSingleton(InMemoryBookingAdapter.GetCreateBookingDependencies());
services.AddSingleton(InMemoryBookingAdapter.GetCancelBookingDependencies());
This means whenever a certain dependency is needed (e.g. by a handler or endpoint), the DI container will supply the singleton instance we created. Also, Bootstrap()
is called once to load initial data into the in-memory store . With this setup, our system is ready to handle requests.
Handlers – Orchestrating Use-Case Logic
Each use-case has a handler function that contains the core decision-making and orchestration, using the ports for any outside calls. In PASTA, handlers are just static methods (or could even be local functions or lambdas) – no base classes or inheritance needed. They typically accept the input parameters for the use-case plus the ...Dependencies (the group of port functions). The handler then executes the business logic, which often looks like a sequence of function calls with some if checks for validations. Crucially, all heavy lifting (data retrieval, persistence) is delegated to the ports, so the handler itself is mostly “glue” logic – which aligns with the imperative shell idea.
Let’s examine two handlers: CreateBooking and CancelBooking.
CreateBookingHandler: This handles the process of booking a member into a class. Its signature is
Handle(Guid memberId, Guid classId, CreateBookingDependencies deps)
. Inside, it performs a series of checks and operations:Get Member: It calls
deps.GetMember(memberId)
to fetch the member. If the result is failure (e.g. no such member), it returns that failure immediately . If the member exists but is not active, it returns aMemberInactive
error . Notice how using the Result type makes it easy to propagate errors up – we can just return a Fail(...) result and the handler’s caller will get it.Get Class: Next, it calls
deps.GetClass(classId)
to fetch the class info . If that fails (class not found), it bubbles that up similarly .Business Rules: It then uses a couple of port functions to enforce business rules: it calls
deps.HasExistingBooking(memberId, classId)
– if that returns true, it means the member is already booked for that class, so it returns anAlreadyBooked
error . It also callsdeps.IsClassFull(classId)
, and if that is true, returns aClassFull
error . These checks ensure no duplicate booking and no overbooking beyond capacity.Create & Save Booking: If all checks pass, the handler creates a new Booking object (with a new GUID and timestamp) and then calls
deps.SaveBooking(booking)
to persist it . Finally, it returns the created booking as a success result.
All of the above is done in ~30 lines of straightforward code – no dependency lookups, no try-catch for expected conditions (because we use Result for flow), and no direct database code. The handler doesn’t know if data came from a DB or in-memory list; it doesn’t care. It focuses on business logic flow: get data, validate, decide, and call outputs.
CancelBookingHandler: This one is even simpler. Its job is to cancel an existing booking by ID. The code roughly does:
var bookingResult = await deps.GetBooking(bookingId); if (!bookingResult.IsSuccess) return bookingResult; // if booking not found, return error await deps.CancelBooking(bookingId); return bookingResult; // return the original booking as confirmation
As we see, it first tries to fetch the booking via the GetBooking
port. If that yields a BookingNotFound
error, it simply returns that error upward (no further action). If a booking was found, it calls the CancelBooking
delegate to perform the cancellation (in our adapter, this removes it from the list). Then it returns the Result containing the original booking (this could be used by the caller to, say, display what was canceled). This handler doesn’t itself contain any complex logic – it delegates to the port functions entirely. The benefit is that to test this handler, we could give it a fake GetBooking
that returns whatever we want, and a fake CancelBooking
that maybe just sets a flag – then verify it returns the right result. There’s no need for any mocking of infrastructure; we can inject simple functions.
Shell (Entry Point)
On the very outside, we have the ASP.NET Core minimal API configuring HTTP endpoints. In PASTA, this is the inbound adapter connecting HTTP to our handlers. In the sample’s Program.cs, the endpoints are set up like:
GET /classes
→ returns all classes with availability. (This simply calls the adapter’sGetClassAvailability()
which computes available slots from the in-memory data – essentially a read-through with no extra orchestration needed.)GET /members
→ returns all members (directly from the adapter’s in-memory list).POST /bookings
→ creates a booking. This endpoint needs to callCreateBookingHandler
. Using minimal APIs, we can directly inject our dependencies and bind the request body to a DTO. For example:app.MapPost("/bookings", async (BookingRequest req, CreateBookingDependencies deps) => await CreateBookingHandler.Handle(req.MemberId, req.ClassId, deps));
. The DI system will provide the deps object we registered, and the handler returns aResult<Booking, BookingError>
which ASP.NET can translate to an HTTP response (e.g. error results might become 400 Bad Request with an error code/message).DELETE /bookings/{id}
→ cancels a booking by ID. Similarly, this callsCancelBookingHandler.Handle(id, deps)
and returns the result.
For the GET endpoints, since they don’t need complex logic, the code just calls the adapter functions directly (as a shortcut). For instance, app.MapGet("/members", () => InMemoryBookingAdapter.GetAllMembers());
returns the list of members from memory . In a more complex scenario, you could have a dedicated handler for a query as well, but it wasn’t necessary here.
Notice that each route is essentially pointing either directly to an adapter function or to a handler function with its dependencies injected. There are no controller classes, no broad service layers – the mapping is very direct from HTTP to the core logic. This keeps the shell extremely thin. It mostly just converts HTTP requests to method calls and vice versa (e.g. converting the Result to an appropriate HTTP response).
Benefits of PASTA (and Considerations)
By now, the advantages of the PASTA approach should be emerging clearly:
High Cohesion, Low Coupling: Each feature/use-case is self-contained. All the code for booking a class lives in one place (one folder/module), making it easy to maintain and evolve. Changes in one feature tend not to ripple unnecessarily into others, because there isn’t a tangle of shared services – communication happens through clear port contracts.
No Rigid Layer Ceremony: PASTA does away with the “empty” layers that plague some Clean Architecture projects. If your domain logic for a use-case can be written in one function, PASTA lets you do exactly that. You don’t need to create a Repository interface, then an implementation, then a service class calling that – you can just pass a function to the handler. This significantly reduces boilerplate. The architecture is structure when needed, not structure for its own sake.
Testability by Design: Because core logic is isolated in functions and relies on abstract delegates for external interactions, testing is straightforward. Unit tests can be written for the pure functions (e.g. validations, calculations in the core) and even for handlers by supplying simple fake delegates. In fact, PASTA encourages writing unit tests for all core logic and any side-effect-free handlers . You don’t need a mocking library to fake an interface – you can literally pass a lambda or use a local function to simulate a database call. For higher-level confidence, PASTA also promotes component testing of each vertical slice – essentially an integration test that runs a use-case end-to-end with its real adapters (maybe an in-memory database or a test double that is closer to real) . This way you test the slice in integration, but still without involving external systems you don’t control (e.g. you wouldn’t call a real SMTP service in a test; you’d plug in a fake email adapter). This testing strategy gives you both fast unit tests and meaningful integration tests with minimal friction. As the manifesto puts it: “No need for mocks when logic is in pure functions… Test core logic in isolation… Handlers are easy to test with hand-written or inline dependencies.”
Clarity and Maintainability: PASTA’s vertical slice structure often means when you read the code, you’re reading the business logic directly, not jumping through indirection. A developer can open one file and see: here’s what happens when a booking is created – first these validations, then this creation, then saving. The intent is clear and the code reads like the use-case. This aligns with the original goals of Clean Architecture (keeping use-cases central) but achieves it in a more concrete, less abstract way. Moreover, because each slice has explicit dependencies passed in, it’s very transparent what that code relies on. This makes refactoring safer – you know exactly what you can change without breaking contracts.
Of course, no architecture is a silver bullet. There are some considerations and potential drawbacks to watch out for with PASTA:
Learning Curve for Delegates: For teams used to classic OOP patterns with interfaces, the heavy use of delegates (function pointers) might be unfamiliar. It’s essentially functional programming idioms in C#. However, it usually clicks quickly once you see that a delegate is just an interface with a single method, minus the interface boilerplate. Modern C# even has
Func<..>
andAction<..>
which are similar; PASTA just uses public delegate for stronger typing and naming.Organizing Large Codebases: In a very large system, hundreds of vertical slices could become hard to navigate if not organized logically (e.g. by bounded contexts or feature areas). PASTA doesn’t forbid grouping slices into larger modules – in practice you might still have folders or namespaces for broader domains. The key is it avoids grouping by layer. So, one must design the slices and their boundaries thoughtfully (which is true for any modularization approach).
Framework Integration: PASTA is quite easy to integrate with frameworks like ASP.NET Core due to its minimalist approach (just plug delegates into DI and call handlers). But if a framework is very convention-based (scans for controllers, etc.), you might have to either bypass some conventions or write a bit of glue. In .NET this is not a big issue with minimal APIs and the flexibility of the middleware pipeline.
Overall, PASTA has shown itself to be a pragmatic architecture that keeps the codebase flexible and clean as it grows, without the tedium that sometimes accompanies Clean/Onion implementations. It strikes a balance between no structure (just ad-hoc code) and over-engineered structure (too many layers & indirections). By borrowing the best from hexagonal and vertical slice patterns, it allows you to defer unnecessary abstraction until it’s needed – but encourages good habits like separation of pure logic from side-effects from the start.
Sources:
PASTA Architecture – GitHub (explains PASTA concepts, manifesto, and code examples)
“Clean Architecture Disadvantages” (discusses issues with traditional Clean Architecture in practice)
“Onion Architecture” (notes on onion architecture drawbacks in project structure)
“Functional Core, Imperative Shell” (introduction to the functional core/imperative shell concept)
Vertical Slice Architecture (promotes organizing code by feature slices for high cohesion)
Subscribe to my newsletter
Read articles from Danyl Novhorodov directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Danyl Novhorodov
Danyl Novhorodov
I’m an experienced software developer and technical leader working in the software industry since 2004. My interests are in software architecture, functional programming, web development, DevOps, cloud computing, Agile, and much more.