Architecture Deployment API

Development View

Describes the system’s source code organisation, module boundaries, dependency rules, and the conventions used by developers working on the codebase.

Module / Project Structure

The backend is split into four .NET projects that follow Clean Architecture. Dependency arrows always point inward — API and Infrastructure depend on Application, which depends only on Domain. The Domain project has zero external NuGet dependencies: it contains only pure C# business logic, aggregates, and value objects. The three portal projects (AdminPortal, VendorPortal, CustomerApp) are fully separate ASP.NET applications that communicate with the backend exclusively via the REST API over HTTP — they hold no direct project references to any backend assembly.

flowchart TD
  subgraph Backend["src/backend/"]
    API["FlowerShop.API\n─────────────\nControllers (23)\nMiddleware pipeline\nProgram.cs / DI root\nDTO request/response types"]
    APP["FlowerShop.Application\n─────────────\nCommands + Handlers\nQueries + Handlers\nDomain Services\nDTOs (application)\nFluentValidation validators\nMediatR behaviours\nEvent handlers"]
    DOM["FlowerShop.Domain\n─────────────\nAggregates (14)\nValue Objects\nDomain Events\nRepository interfaces\nIDs (strongly-typed)\nResult<T>"]
    INF["FlowerShop.Infrastructure\n─────────────\nEF Core DbContext\nEntity configurations\nRepositories\nKeycloak client\nMQTT services\nSignalR hub\nRedis cache\nEvent bus\nDI extensions"]
  end

  subgraph Portals["src/portals/"]
    ADM["FlowerShop.AdminPortal\n(Razor Pages :5001)"]
    VND["FlowerShop.VendorPortal\n(Razor Pages :5002)"]
    CUS["FlowerShop.CustomerApp\n(ASP.NET MVC :5003)"]
  end

  subgraph Tests["tests/"]
    UT["FlowerShop.Tests.Unit"]
    IT["FlowerShop.Tests.Integration"]
    DT["FlowerShop.Domain.Tests"]
  end

  API --> APP
  API --> DOM
  APP --> DOM
  INF --> APP
  INF --> DOM
  ADM --> API
  VND --> API
  CUS --> API

Sub-domain Mapping — Projects

Project Business Sub-domain(s) What lives there
FlowerShop.Domain All sub-domains Pure business logic: aggregates (Bouquet, SmartVase, Order, Vendor, Customer, …), value objects (Money, Location, FreshnessScore), domain events, strongly-typed IDs
FlowerShop.Application All sub-domains Use-case layer: commands/queries per sub-domain (Vendor, Bouquet, Order, Cart, Customer, IoT, Analytics, Notification), MediatR pipeline behaviours
FlowerShop.Infrastructure Identity & Tenancy, IoT/MQTT, Payments, Vendor Management EF Core repositories, Keycloak client, MQTT services, Redis cache, Stripe stub, FCM stub, SignalR hub, RabbitMQ/InMemory event bus
FlowerShop.API All sub-domains Controllers (23), middleware pipeline, DI root — thin HTTP adapter over Application layer
FlowerShop.AdminPortal (:5001) Platform Administration Razor Pages UI for vendor approval/suspension, vase registration, platform analytics
FlowerShop.VendorPortal (:5002) Vendor Management / Vendor Order Fulfilment Razor Pages UI for bouquet catalogue, vase management, order fulfilment
FlowerShop.CustomerApp (:5003) Customer Purchasing ASP.NET MVC UI for bouquet browsing, cart, checkout, Leaflet map
FlowerShop.Tests.Unit All sub-domains Handler and behaviour unit tests (mocked repositories)
FlowerShop.Tests.Integration All sub-domains Full pipeline tests against real PostgreSQL (TestContainers)
FlowerShop.Domain.Tests All sub-domains Aggregate invariant and state-machine tests (no infrastructure)

Dependency rule: arrows flow inward only. Domain has zero external dependencies. Application depends only on Domain. Infrastructure and API depend on Application + Domain. Portals call the REST API over HTTP — they do not reference backend projects directly.

Sub-domain Mapping — MediatR Pipeline

Behaviour Business Sub-domain What it enforces
LoggingBehaviour All sub-domains Traces every command/query with request ID, duration, and result
ValidationBehaviour All sub-domains Runs FluentValidation rules — rejects malformed bouquet prices, missing vendor IDs, invalid coordinates
TenantValidationBehavior Identity & Tenancy Ensures ITenantScopedRequest commands carry a valid tenant context before the handler runs
IdempotencyKeyBehaviour Customer Purchasing / Payments Deduplicates order and payment commands using Redis SET NX before the handler executes
CachingBehaviour Customer Purchasing / Vendor Management Serves ICacheableQuery results (bouquet search, vendor dashboards) from Redis; invalidated by version-key increment

CQRS — File Conventions

FlowerShop.Application/
  Commands/
    <Domain>/
      Create<Entity>Command.cs        ← record + IRequest<Result<Guid>>
      Create<Entity>CommandHandler.cs ← IRequestHandler<>
      Update<Entity>Command.cs
      Update<Entity>CommandHandler.cs
  Queries/
    <Domain>/
      Get<Entity>Query.cs             ← record + IRequest<Result<EntityDto>>
      Get<Entity>QueryHandler.cs
  Validators/
    <Domain>/
      Create<Entity>CommandValidator.cs  ← AbstractValidator<>
  DTOs/
    <EntityDto>.cs
  Events/
    <DomainEvent>Handler.cs            ← INotificationHandler<>

MediatR Pipeline Behaviours (execution order)

flowchart LR
  REQ["IRequest sent\nvia ISender.Send()"]
  --> LB["LoggingBehaviour\n(log request + response time)"]
  --> VB["ValidationBehaviour\n(FluentValidation; returns\nValidationError on failure)"]
  --> TVB["TenantValidationBehavior\n(enforces tenant context\nfor ITenantScopedRequest)"]
  --> IKB["IdempotencyKeyBehaviour\n(checks Redis before handler)"]
  --> CB["CachingBehaviour\n(for ICacheableQuery — read from Redis)"]
  --> HAND["Command / Query Handler"]

Domain Layer Conventions

Convention Rule
Aggregate root Inherits AggregateRoot<TId> — only root is reachable from outside
Value objects Inherit ValueObject; immutable; equality by value
Strongly-typed IDs VendorId, BouquetId, CustomerId, OrderId, VaseId, UserId — wrap Guid; prevent cross-aggregate ID confusion
**Result** Domain methods return Result<T> — never throw for business-rule violations
Domain events Raised via AggregateRoot.AddDomainEvent(); dispatched through outbox
No infrastructure Domain project has zero NuGet dependencies on EF Core, Redis, etc.

Infrastructure — EF Core Conventions

Each aggregate has a dedicated IEntityTypeConfiguration<T> class under:

FlowerShop.Infrastructure/Persistence/Configurations/<Entity>Configuration.cs
  • Value objects are mapped with OwnsOne / OwnsMany
  • Strongly-typed IDs are converted via HasConversion<Guid>
  • No navigation properties on aggregates (cross-aggregate references use IDs only)

Key Configuration Files

File Purpose
src/backend/FlowerShop.API/Program.cs DI root, middleware pipeline registration
src/backend/FlowerShop.Infrastructure/Extensions/ServiceCollectionExtensions.cs All infrastructure DI registrations incl. stubs
src/backend/FlowerShop.Infrastructure/Persistence/FlowerShopDbContext.cs EF Core DbContext, outbox_messages table
src/backend/FlowerShop.API/appsettings.json App configuration (override via env vars in Docker)
docker/docker-compose.dev.yml Local dev infrastructure

Test Strategy

Project What it tests Approach
FlowerShop.Domain.Tests Aggregate invariants, value-object equality, domain state machines xUnit, no infrastructure
FlowerShop.Tests.Unit Command/query handlers, MediatR behaviours, validators xUnit, mocked repositories
FlowerShop.Tests.Integration EF Core repositories, full MediatR pipeline, controller endpoints xUnit, real PostgreSQL via TestContainers

Current status: 126/126 tests passing.

Migration Workflow

# From Infrastructure project
dotnet ef migrations add <Name> --startup-project ../FlowerShop.API

# From API project
dotnet ef database update

Known gap: Cart, CartItem, OrderLine (multi-line schema), Subscription, Favourite, Notification, DeviceRegistration, SavedAddress tables require AddCartAndOrderLines migration — not yet run.