Skip to content

Architecture Overview

Tech Stack

LayerTechnologies
Backend.NET 10, Minimal APIs (REPR pattern with IEndpoint), EF Core + PostgreSQL 18, FluentValidation
AuthASP.NET Core native (JWT Bearer, HttpOnly cookies), Axios interceptors + Pinia
CacheFusionCache + Redis
MessagingIn-process event dispatch (InProcessEventBus, zero latency, zero DB for integration events)
StorageMinIO (S3-compatible, pinned, never internet-exposed)
AnalyticsClickHouse (audit, events)
ObservabilitySerilog, OpenTelemetry, Scalar (API docs)
ResiliencePolly (retries, circuit breaker, timeouts)
EmailMailKit (SMTP) + React Email + Hono/Bun (renderer)
FrontendVue 3 Composition API, Pinia 3, Vue Router, Vite, TypeScript strict
UIshadcn-vue, Tailwind CSS v4, lucide-vue-next
FormsVeeValidate + Zod
Lint/Formatoxlint, oxfmt
TestingxUnit v3, Testcontainers (backend) / Vitest, Playwright (frontend)

Design Patterns

Vertical Slices + Screaming Architecture + Modular Monolith

The backend follows a Vertical Slices architecture where each feature is a self-contained folder with its own Endpoint, Handler, and Validator. Folder names reflect business capabilities, not technical layers. The entire application ships as a single deployable unit with strict module boundaries enforced by architecture tests.

src/
├── Wilo.Api/              Composition root (ASP.NET host, references all modules)
├── Wilo.Common/           Shared kernel (zero project references, pure utilities)
├── Wilo.Contracts/        Inter-module contracts (events, requests, shared DTOs)
├── Wilo.Infrastructure/   Technical cross-cutting concerns (email, storage, outbox, audit)
├── Wilo.Modules.Identity/ Authentication & Identity
├── Wilo.Modules.Members/  Member Management
└── Wilo.Modules.Storage/  File storage endpoints

Dependency Rules

                    Wilo.Api
            (Composition Root — refs ALL)
               /        |        \
              v         v         v
      Wilo.Modules.*  Wilo.Infrastructure
            |    \         |
            v     v        v
      Wilo.Common  Wilo.Contracts
                      |
                      v
                 Wilo.Common
                (zero refs)
ProjectCan referenceCANNOT reference
Wilo.CommonNuGet packages onlyAny project
Wilo.ContractsWilo.CommonModules, Infrastructure, Api
Wilo.InfrastructureWilo.Common, Wilo.ContractsModules, Api
Wilo.Modules.*Wilo.Common, Wilo.Contracts, Wilo.InfrastructureOther modules, Api
Wilo.ApiEverything

Critical rule: No cross-module references. Module A never references Module B. All inter-module communication goes through Wilo.Contracts types.

Feature Slice Pattern (REPR)

Each feature lives in its own folder and contains up to three files:

Wilo.Modules.{Module}/Features/{Operation}/
├── {Op}Endpoint.cs      IEndpoint + nested {Op}Request / {Op}Response records
├── {Op}Handler.cs       Plain class with HandleAsync (auto-registered by Scrutor)
└── {Op}Validator.cs     AbstractValidator<{Op}Endpoint.{Op}Request>

Handlers are plain classes — no CQRS interfaces, no MediatR, no decorator chains. Scrutor scans and registers by naming convention (*Handler).

Module Anatomy

Each module exposes a single registration class:

csharp
public static class MembersModuleRegistration
{
    // Registers DbContext, handlers, validators, consumers, event handlers in DI
    public static WebApplicationBuilder AddMembersModule(this WebApplicationBuilder builder) { ... }

    // Maps all IEndpoint implementations from the module's assembly
    public static IEndpointRouteBuilder MapMembersEndpoints(this IEndpointRouteBuilder app) { ... }
}

Each module has its own PostgreSQL schema and DbContext:

csharp
public sealed class MembersDbContext(DbContextOptions<MembersDbContext> options) : DbContext(options)
{
    public const string SchemaName = "members";
    public DbSet<Member> Members => Set<Member>();
}

Event Dispatch (In-Process)

Integration events are dispatched synchronously in-process via InProcessEventBus. No external message broker, no polling, no DB rows per integration event.

Handler calls db.SaveChangesAsync()
  → OutboxInterceptor (SaveChangesInterceptor):
      1. Harvest domain events from ChangeTracker.Entries<Entity>()
      2. Dispatch to IDomainEventHandler<T> via DomainEventDispatcher
         → Handlers call InProcessEventBus.PublishAsync() → IEventConsumer<T> (synchronous, in-process)
         → Handlers call auditOutbox.Stage()
      3. INSERT audit outbox_messages (same DB transaction — atomic)
  → Transaction committed
  → [Background] AuditOutboxProcessor → ClickHouse (audit pipeline only)

Integration events are dispatched with zero latency within the same request scope. The outbox is used exclusively for the audit pipeline.

Inter-Module Communication

Sync (IModuleClient) — when the caller needs an immediate response:

csharp
var result = await moduleClient.SendAsync(new GetMemberByUserIdRequest(userId), ct);

Async (IModuleEventBus) — fire-and-forget, dispatched synchronously in-process:

csharp
// Dispatched in-process to all IEventConsumer<T> handlers during SaveChangesAsync
await eventBus.PublishAsync(new MemberSubscribedEvent { ... }, ct);
await db.SaveChangesAsync(ct); // entity changes committed, audit events persisted atomically

Consumers implement IEventConsumer<T> and are auto-registered by Scrutor.

Infrastructure Services

┌──────────────────────────────────────────────────────┐
│                      Wilo.Api                         │
│              ASP.NET 10 + Minimal APIs                │
│                     Port 5000                         │
└──┬─────┬──────┬──────┬──────┬──────┬──────────────────┘
   │     │      │      │      │      │
   ▼     ▼      ▼      ▼      ▼      ▼
Postgres Redis MinIO  Click  Email  Aspire
 :5432  :6379 :9000  House  Rend.  Dashboard
                     :8123  :3050  :18888


                            Mailpit
                            :1025
ServicePurposeKey Configuration
PostgreSQL 18Main relational databaseEF Core + NodaTime + SnakeCaseConvention, one schema per module
RedisDistributed cacheFusionCache L1 (memory) + L2 (Redis) + backplane
MinIOS3-compatible object storageBuckets: wilo-documents, wilo-images, wilo-temp
ClickHouseAnalytical databaseImmutable audit_logs table (MergeTree engine)
Email RendererReact Email templatesHono HTTP server on Bun runtime
MailpitDev email testingSMTP capture on port 1025, Web UI on port 8025
Aspire DashboardOpenTelemetry UITraces, metrics, logs visualization on port 18888

Health Checks

Three health check endpoints follow cloud-native best practices:

EndpointPurposeExternal Checks
GET /healthAggregated status (cached 10s)All dependencies
GET /health/liveLiveness probeNone (process responsive only)
GET /health/readyReadiness probe (3s timeout each)PostgreSQL, Redis, MinIO, ClickHouse, Email Renderer

Response format:

json
{
  "status": "Healthy",
  "totalDuration": "00:00:01.234",
  "entries": {
    "postgres": { "status": "Healthy" },
    "redis": { "status": "Healthy" }
  }
}

Prohibited Technologies

ProhibitedUse Instead
BCrypt, Argon2idPBKDF2-SHA512 (PasswordHasher)
SwaggerScalar
FastEndpointsMinimal APIs (IEndpoint pattern)
MVC controllersMinimal APIs (IEndpoint pattern)
MassTransit, RabbitMQOutbox Pattern
MediatRPlain handler classes, no CQRS interfaces
AutoMapperManual mapping or extension methods
DateTime / DateTimeOffsetNodaTime (Instant, LocalDate)
Guid.CreateVersion7()Guid7.NewGuid() wrapper (Medo.Uuid7)
ESLint, Prettier, Biomeoxlint, oxfmt
Options API (Vue)Composition API with <script setup>
Chart.jsRadix UI
WebpackVite
Razor (email)React Email