17 KiB
Architecture
This document describes the active architecture of The Biergarten App.
High-Level Overview
The Biergarten App is a monorepo with a clear split between the backend and the active website:
- Backend: .NET 10 Web API with SQL Server, organized as vertical feature slices with MediatR
- Frontend: React 19 + React Router 7 website in
web/frontend - Architecture Style: Vertical-slice backend plus server-rendered React frontend
The legacy Next.js frontend has been retained in archive/next-js-web-app/
for reference only.
Diagrams
For visual representations, see:
- architecture.svg - Vertical-slice architecture diagram
- deployment.svg - Docker deployment diagram
- authentication-flow.svg - Authentication workflow
- database-schema.svg - Database relationships
Backend Architecture
Vertical Slice Architecture Pattern
The backend organizes business capabilities as feature slices instead of
technical layers. Each feature (Features.Auth, Features.Breweries,
Features.UserManagement, Features.Emails) is a single project that owns
its own controller, MediatR commands/queries/handlers, validators, and
repository, end to end:
┌───────────────────────────────────────────────────────────────────────┐
│ API.Core (thin host) │
│ - Program.cs wiring: MediatR + AddApplicationPart per slice │
│ - Swagger/OpenAPI, JWT auth middleware, global exception filter │
└───────────────────────────────────────────────────────────────────────┘
↓ discovers controllers via AddApplicationPart
┌───────────────┬───────────────┬───────────────────┬───────────────────┐
│ Features.Auth │Features. │ Features. │ Features.Emails │
│ │Breweries │ UserManagement │ (no controller, │
│ Controller │ Controller │ Controller │ internal only) │
│ Commands/ │ Commands/ │ Commands/ │ Commands/ │
│ Queries + │ Queries + │ Queries + │ Handlers │
│ Handlers │ Handlers │ Handlers │ │
│ Repository │ Repository │ Repository │ EmailDispatcher │
└───────────────┴───────────────┴───────────────────┴───────────────────┘
↓ each slice depends only on shared/domain/infra, never on another slice
┌─────────────────────────┬─────────────────────────┬────────────────────┐
│ Shared.Contracts │ Shared.Application │ Domain.Entities / │
│ (ResponseBody envelope) │ (ValidationBehavior, │ Domain.Exceptions │
│ │ cross-slice email cmds) │ │
└─────────────────────────┴─────────────────────────┴────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────┐
│ Infrastructure.Sql, Infrastructure.Jwt, Infrastructure.PasswordHashing, │
│ Infrastructure.Email, Infrastructure.Email.Templates │
└─────────────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Database (SQL Server) │
│ - Stored procedures │
│ - Tables & constraints │
└─────────────────────────────────────┘
Slices never reference each other's project. The one cross-slice
interaction (Features.Auth triggering a confirmation email handled by
Features.Emails) goes through a MediatR command (SendRegistrationEmailCommand)
whose contract lives in Shared.Application, so neither slice takes a
project reference on the other.
Layer Responsibilities
API Layer (API.Core)
Purpose: Thin ASP.NET Core host: no business logic, no controllers of its own
Components:
Program.cs: registers MediatR (scanning everyFeatures.*assembly), FluentValidation, and usesAddApplicationPartso each slice's controllers are discovered by MVCGlobalException.cs: global exception filter (host-level cross-cutting concern)Authentication/JwtAuthenticationHandler.cs: JWT auth scheme middleware- Swagger/OpenAPI documentation, health check endpoints
Dependencies:
- Every
Features.*project (for controller/MediatR discovery) Shared.Contracts,Shared.ApplicationInfrastructure.Jwt(for the auth middleware)
Rules:
- No controllers, no business logic, no feature-specific contracts
- Exists purely to host and wire up the feature slices
Feature Slices (Features.Auth, Features.Breweries, Features.UserManagement, Features.Emails)
Purpose: Each slice is the complete vertical for one business capability
Components (per slice):
Controllers/: HTTP endpoints, binding directly to Command/Query types as the request contractCommands/<Operation>/andQueries/<Operation>/: one folder per operation, each containing the Command/Query record, itsIRequestHandler, and (for commands) a FluentValidation validatorRepository/: the slice's own stored-procedure repository implementationDtos/: response shapes returned by query handlers (never the raw domain entity)DependencyInjection/: anAddFeaturesX()extension method registering the slice's repository/services
Features.Emails has no Controllers/ folder. It's invoked only via
MediatR commands sent from other slices, never over HTTP.
Dependencies:
Domain.Entities,Domain.ExceptionsInfrastructure.Sql(generic ADO.NET plumbing) plus whichever infrastructure project the slice needs (Infrastructure.Jwt/Infrastructure.PasswordHashingfor Auth,Infrastructure.Email/Infrastructure.Email.Templatesfor Emails)Shared.Contracts,Shared.Application- Never another
Features.*project
Rules:
- All business logic for that feature lives in its command/query handlers
- No direct controller-to-repository calls; everything flows through MediatR
- Read endpoints return a dedicated
Dto, never the domain entity directly
Shared Projects (Shared.Contracts, Shared.Application)
Purpose: The minimum cross-slice surface area required because every slice needs it, or because duplicating it four times would be worse than sharing it
Components:
Shared.Contracts:ResponseBody<T>/ResponseBody, the API response envelope every controller returnsShared.Application:ValidationBehavior<TRequest,TResponse>(the MediatR pipeline behavior that runs FluentValidation before a handler executes) and the cross-slice email commands (SendRegistrationEmailCommand,SendResendConfirmationEmailCommand)
Rules:
- Kept deliberately small: this is the exception to "no slice depends on another slice," not a general-purpose dumping ground
Infrastructure Layer
Purpose: Technical capabilities and external integrations, shared by whichever slices need them
Components:
- Infrastructure.Sql: generic ADO.NET connection/command plumbing
(
ISqlConnectionFactory, the abstractRepository<T>base class), not domain-specific; each slice's ownRepository/folder builds on this - Infrastructure.Jwt: JWT token generation and validation
- Infrastructure.PasswordHashing: Argon2id password hashing
- Infrastructure.Email: Email sending capabilities (SMTP/MailKit)
- Infrastructure.Email.Templates: Email template rendering (Razor components)
Dependencies:
- Domain entities
- External libraries (ADO.NET, JWT, Argon2, MailKit, etc.)
Rules:
- Implements technical concerns only, no business logic
- Reusable across slices
Domain Layer (Domain.Entities)
Purpose: Core business entities and models
Components:
UserAccount- User profile dataUserCredential- Authentication credentialsUserVerification- Account verification state
Dependencies:
- None (pure domain)
Rules:
- Plain Old CLR Objects (POCOs)
- No framework dependencies
- No infrastructure references
- Represents business concepts
Design Patterns
Vertical Slice + MediatR
Purpose: Organize code by feature instead of by technical layer, so everything needed to understand or change one capability lives in one project
Implementation:
- Each HTTP write operation is a
Command(e.g.CreateBreweryCommandinFeatures.Breweries/Commands/CreateBrewery/); each read operation is aQuery(e.g.GetBreweryByIdQuery) - Controllers bind directly to the Command/Query as the request body; there is no separate request DTO + mapping step for writes
- A single shared
ValidationBehavior<TRequest,TResponse>(Shared.Application/Behaviors/) runs FluentValidation validators in the MediatR pipeline before any handler executes - Query handlers map to a dedicated response
Dto, so domain entities never leak over the wire
Example:
public record CreateBreweryCommand(Guid PostedById, string BreweryName, string Description, ...)
: IRequest<BreweryDto>;
public class CreateBreweryHandler(IBreweryRepository repository) : IRequestHandler<CreateBreweryCommand, BreweryDto>
{
public async Task<BreweryDto> Handle(CreateBreweryCommand request, CancellationToken cancellationToken) { ... }
}
Repository Pattern
Purpose: Abstract database access behind interfaces
Implementation: each slice owns its own repository, scoped to that feature only:
Features.Auth/Repository/IAuthRepository.csFeatures.Breweries/Repository/IBreweryRepository.csFeatures.UserManagement/Repository/IUserAccountRepository.csInfrastructure.Sql/DefaultSqlConnectionFactory.cs: the generic connection factory every slice's repository builds on
Benefits:
- Testable (easy to mock)
- SQL-first approach (stored procedures)
- Each slice's data access logic is self-contained
Example:
public interface IAuthRepository
{
Task<UserCredential> GetUserCredentialAsync(string username);
Task<int> CreateUserAccountAsync(UserAccount user, UserCredential credential);
}
Dependency Injection
Purpose: Loose coupling and testability
Configuration: Program.cs wires up MediatR/FluentValidation across
every Features.* assembly; each slice exposes its own AddFeaturesX()
extension method that registers its repository and slice-internal services
Lifetimes:
- Scoped: Repositories, slice-internal services (per request)
- Singleton:
ISqlConnectionFactory - Transient: Utilities, helpers
SQL-First Approach
Purpose: Push complex logic into the database
Strategy:
- All queries via stored procedures
- No ORM (Entity Framework not used)
- Database handles complex logic
- Application focuses on orchestration
Stored Procedure Examples:
USP_RegisterUser- User registrationUSP_GetUserAccountByUsername- User lookupUSP_RotateUserCredential- Password update
Frontend Architecture
Active Website (web/frontend)
The current website is a React Router 7 application with server-side rendering enabled.
web/frontend/
├── app/
│ ├── components/ Shared UI such as Navbar, FormField, SubmitButton, ToastProvider
│ ├── lib/ Auth helpers, schemas, and theme metadata
│ ├── routes/ Route modules for home, login, register, dashboard, confirm, theme
│ ├── root.tsx App shell and global providers
│ └── app.css Theme tokens and global styling
├── .storybook/ Storybook config and preview setup
├── stories/ Storybook stories for shared UI and themes
├── tests/playwright/ Storybook Playwright coverage
└── package.json Frontend scripts and dependencies
Frontend Responsibilities
- Render the auth demo and theme guide routes
- Manage cookie-backed website session state
- Call the .NET API for login, registration, token refresh, and confirmation
- Provide shared UI building blocks for forms, navigation, themes, and toasts
- Supply Storybook documentation and browser-based component verification
Theme System
The active website uses semantic DaisyUI theme tokens backed by four Biergarten themes:
- Biergarten Lager
- Biergarten Stout
- Biergarten Cassis
- Biergarten Weizen
All component styling should prefer semantic tokens such as primary,
success, surface, and highlight instead of hard-coded color values.
Legacy Frontend
The previous Next.js frontend has been archived at archive/next-js-web-app/
for reference only. Active product and engineering documentation should point
to web/frontend.
Security Architecture
Authentication Flow
-
Registration:
- User submits credentials
- Password hashed with Argon2id
- User account created
- JWT token issued
-
Login:
- User submits credentials
- Password verified against hash
- JWT token issued
- Token stored client-side
-
API Requests:
- Client sends JWT in Authorization header
- Middleware validates token
- Request proceeds if valid
Password Security
Algorithm: Argon2id
- Memory: 64MB
- Iterations: 4
- Parallelism: CPU core count
- Salt: 128-bit (16 bytes)
- Hash: 256-bit (32 bytes)
JWT Tokens
Algorithm: HS256 (HMAC-SHA256)
Claims:
sub- User IDunique_name- Usernamejti- Unique token IDiat- Issued at timestampexp- Expiration timestamp
Configuration (appsettings.json):
{
"Jwt": {
"ExpirationMinutes": 60,
"Issuer": "biergarten-api",
"Audience": "biergarten-users"
}
}
Database Architecture
SQL-First Philosophy
Principles:
- Database is source of truth
- Complex queries in stored procedures
- Database handles referential integrity
- Application orchestrates, database executes
Benefits:
- Performance optimization via execution plans
- Centralized query logic
- Version-controlled schema (migrations)
- Easier query profiling and tuning
Migration Strategy
Tool: DbUp
Process:
- Write SQL migration script
- Embed in
Database.Migrationsproject - Run migrations on startup
- Idempotent and versioned
Migration Files:
scripts/
├── 001-CreateUserTables.sql
├── 002-CreateLocationTables.sql
├── 003-CreateBreweryTables.sql
└── ...
Data Seeding
Purpose: Populate development/test databases
Implementation: Database.Seed project
Seed Data:
- Countries, states/provinces, cities
- Test user accounts
- Sample breweries (future)
Deployment Architecture
Docker Containerization
Container Structure:
sqlserver- SQL Server 2022database.migrations- Schema migration runnerdatabase.seed- Data seederapi.core- ASP.NET Core Web API
Environments:
- Development (
docker-compose.dev.yaml) - Testing (
docker-compose.test.yaml) - Production (
docker-compose.prod.yaml)
For details, see Docker Guide.
Health Checks
SQL Server: Validates database connectivity API: Checks service health and dependencies
Configuration:
healthcheck:
test: ["CMD-SHELL", "sqlcmd health check"]
interval: 10s
retries: 12
start_period: 30s
Testing Architecture
Test Pyramid
┌──────────────┐
│ Integration │ ← API.Specs (Reqnroll)
│ Tests │
├──────────────┤
│ Unit Tests │ ← Features.Auth.Tests, Features.Breweries.Tests,
│ (per slice, │ Features.UserManagement.Tests, Features.Emails.Tests
│ handlers + │ (commands/queries/handlers + that slice's own
│ repository) │ repository, mocked with Moq/DbMocker)
└──────────────┘
Strategy:
- Many unit tests (fast, isolated)
- Fewer integration tests (slower, e2e)
- Mock external dependencies
- Test database for integration tests
For details, see Testing Guide.