Web application architecture is the blueprint for how the parts of your system - the browser, the server, the API, the database and the services in between - are organised and how they talk to each other. Get it right and new features slot in cleanly, the app stays fast under load, and bugs stay contained. Get it wrong and every change becomes risky, slow and expensive. This guide walks through the decisions that matter most, in plain language for business owners and in enough depth for the developers who will build it.
The four layers of a typical web app
Almost every serious web application is organised into a small number of horizontal layers, each with a single responsibility. Thinking in these layers is the foundation everything else builds on:
- Presentation layer - what the user sees and interacts with: the HTML, CSS and JavaScript running in the browser (for example an Angular front-end).
- API layer - the boundary the front-end calls over HTTP. It receives requests, validates them, and returns data, usually as JSON over a REST API.
- Business (domain) layer - the rules and workflows that actually make your product what it is: pricing, eligibility, order processing, permissions.
- Data-access layer - the code that reads from and writes to your database, isolating the rest of the app from SQL and storage details.
The key idea is that each layer talks only to its immediate neighbour through a clear contract. The browser never touches the database directly; the business layer never knows or cares whether a request arrived over the web or from a background job.
Separation of concerns and clean architecture
Separation of concerns simply means each part of the system does one job and does it well. It is the single most valuable principle in software design because it lets you change one thing without breaking five others.
Clean architecture takes this further by inverting the dependencies. Instead of business logic depending on the database and the web framework, those infrastructure concerns depend inward on the domain through interfaces. Your core business rules end up at the centre, depending on nothing external - which means you can unit-test them without a database, swap SQL Server for another store, or replace the web framework, all without rewriting the rules that matter.
Here is a layered folder structure that keeps those boundaries visible in an ASP.NET Core solution:
// A clean, layered .NET solution
Shop.Domain/ // entities + business rules, no dependencies
Shop.Application/ // use cases, interfaces (IOrderRepository)
Shop.Infrastructure/ // EF Core, external services, implementations
Shop.Api/ // controllers, DTOs, dependency injection
Shop.Tests/ // unit + integration tests
The presentation layer: SPA vs SSR vs static
How you render the front-end has a big effect on performance, SEO and complexity. There are three broad approaches:
Single-page application (SPA)
A framework like Angular or React ships a JavaScript bundle that runs in the browser, fetching data from your API and updating the page without full reloads. SPAs shine for app-like interfaces - dashboards, portals, tools - where rich interactivity matters more than first-load speed or search indexing.
Server-side rendering (SSR)
The server renders HTML for each request so the first paint is fast and search engines see full content immediately. This suits content-heavy and marketing pages where SEO and initial load time are critical. Angular offers SSR through its server-rendering support, and on the React side Next.js is the framework most teams reach for - it adds SSR, routing and API routes on top of React, giving you the interactivity of a SPA with a fast, crawlable first response.
Static generation
Pages are pre-built at deploy time and served as plain HTML from a CDN. It is the fastest and cheapest option for content that does not change per user - blogs, documentation and brochure sites. Next.js also supports this mode, plus incremental static regeneration for content that updates periodically without a full rebuild.
Most real products mix these: a static or server-rendered marketing site, and a SPA behind the login. If you are weighing this against an off-the-shelf theme, our guide on custom development versus templates covers the trade-offs.
The back-end API layer
The API is the contract between your front-end and everything behind it. A well-designed REST API built with ASP.NET Core exposes resources at predictable URLs, uses HTTP verbs correctly (GET to read, POST to create, PUT/PATCH to update, DELETE to remove), and returns meaningful status codes. Keep controllers thin - their job is to validate input, call the business layer and shape the response, nothing more.
[ApiController]
[Route("api/orders")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orders;
public OrdersController(IOrderService orders) => _orders = orders;
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var order = await _orders.FindAsync(id);
return order is null ? NotFound() : Ok(order);
}
}
Notice the controller depends on an IOrderService interface, not a concrete class or the database. That is dependency injection doing its job - the controller is trivial to test and the business logic lives where it belongs.
MVC and MVVM patterns
Two patterns come up constantly, and it helps to know where each fits:
- MVC (Model-View-Controller) organises server-side code into models (data), views (output) and controllers (request handling). ASP.NET Core is built around it, and it maps naturally onto a REST API where controllers coordinate work.
- MVVM (Model-View-ViewModel) is a front-end pattern where a view model exposes state and commands that the UI binds to. Angular follows this shape - components hold view-model logic and the template binds to it - which keeps display logic out of your templates.
Monolith vs microservices
This is where teams most often over-engineer. A monolith is a single deployable application containing all your features. A microservices architecture splits the system into many small, independently deployable services that communicate over the network.
Microservices offer independent scaling and deployment and let separate teams own separate services - but they add real cost: network latency, distributed transactions, service discovery, and far harder debugging and monitoring. For the vast majority of products, a modular monolith - one deployable app with clean internal module boundaries - gives you most of the maintainability benefits with none of the distributed-systems tax. Split a service out only when a specific part has a genuine, independent reason to scale or ship on its own schedule.
Stateless services and horizontal scaling
To handle more traffic you generally want to scale horizontally - add more instances of your app rather than a single bigger machine. That only works cleanly if your services are stateless: no request should depend on data held in the memory of one particular server. Keep session state in a shared store (a database or a distributed cache), and any instance can serve any request. A load balancer then spreads traffic across instances, and you can add or remove capacity on demand.
Authentication and authorization
Authentication proves who a user is; authorization decides what they may do. Modern web apps typically handle this with tokens:
- JWT (JSON Web Tokens) - signed tokens the client sends on each request. They let stateless services verify a user without a database lookup, which fits horizontal scaling well.
- OAuth 2.0 / OpenID Connect - the standards for delegated access and single sign-on. OAuth 2.0 handles authorization (issuing access tokens); OpenID Connect layers identity on top so you can sign users in with a trusted provider.
Always transmit tokens over HTTPS, keep their lifetimes short, and validate them on every protected endpoint. For a deeper treatment, see our companion piece on building secure REST APIs with ASP.NET Core.
Caching layers
Caching is one of the highest-leverage tools for performance and cost. Think of it as four layers, from closest to the user outward:
- Client cache - the browser reusing assets and responses it already has.
- CDN cache - static assets and cacheable responses served from an edge location near the user.
- Server cache - an in-memory or distributed cache (such as Redis) holding expensive computed results or frequent queries.
- Database cache - the database's own buffer and query caches, plus well-chosen indexes.
The rule is to cache as close to the user as safely possible, and to invalidate deliberately. Getting the browser and CDN layers right is often the biggest single speed win - something we cover in depth in our website performance optimization guide.
Database design and the data-access layer
Your data model is the part of the system that is hardest to change later, so it deserves care up front. Model your entities and relationships around real business concepts, normalise to avoid duplication, and add indexes to match the queries you actually run. In .NET, Entity Framework Core (EF Core) is the standard object-relational mapper: it maps C# classes to SQL Server tables, generates and runs migrations, and lets you query with LINQ while still dropping to raw SQL when a query needs hand-tuning. Keep EF Core confined to the data-access layer behind repository or service interfaces so the rest of your app never depends on it directly.
Observability and CI/CD
Architecture is not finished when the code compiles - you have to be able to run and evolve it safely.
Observability and logging
You cannot fix what you cannot see. Structured logging, metrics and distributed tracing let you answer “what is slow, what is failing, and why” in production. Log with correlation IDs so a single request can be followed across services, and alert on the signals that reflect user pain - error rates and latency - not just server CPU.
CI/CD
A continuous integration and delivery pipeline builds your code, runs the tests, and deploys automatically on every change. It makes releases repeatable and reversible, catches regressions before they reach users, and removes the fear from shipping. Combined with clean architecture and a good test suite, it is what lets a team move fast without breaking things.
Common mistakes to avoid
- Tight coupling. When every part knows the internals of every other part, no change is local. Depend on interfaces, not concrete classes, and keep layer boundaries strict.
- Fat controllers. Business logic stuffed into API controllers is impossible to reuse and hard to test. Controllers should coordinate, not decide.
- No caching strategy. Recomputing the same result on every request wastes money and time. Decide what is cacheable and for how long, early.
- Premature microservices. Splitting a young product into many services multiplies complexity long before you have the scale or team size to justify it.
Best practices
- Keep layers thin and boundaries strict. Presentation, API, business and data each do one job.
- Depend on abstractions. Use dependency injection so components are swappable and testable.
- Design stateless services. Push shared state into a store so you can scale horizontally.
- Cache deliberately. Cache close to the user and invalidate on purpose, not by accident.
- Automate the boring parts. Tests, builds and deployments belong in a CI/CD pipeline, not in someone's head.
- Instrument from day one. Logs, metrics and traces are cheaper to add early than to retrofit during an outage.
If you would like a second opinion on your architecture - or a partner to build it - our web development service starts with a free discovery call to map the right structure, stack and roadmap for your product.



