Architecture Deployment API

Information View

Describes how the system stores, manages, and flows information. Covers data structure, ownership, lifecycle, and the key transformations data undergoes.

Aggregate Ownership & Tenancy

Every aggregate in the system belongs to exactly one ownership scope, which determines how it is isolated and queried. Platform-scoped aggregates (User, Customer) carry no vendor affiliation. Vendor-scoped aggregates carry both VendorId and VendorType — this pair is the multi-tenancy boundary that prevents one vendor from accessing another’s data. Customer-scoped aggregates belong to a specific customer and encompass all purchasing, preference, and notification data.

flowchart TD
  subgraph PlatformScope["Platform-scoped (no VendorId)"]
    User["User\n(UserId, Role, CustomerId?, VendorId?)"]
    Customer["Customer\n(CustomerId, Name, ContactInfo, Address)"]
    Admin["Platform Admin context"]
  end

  subgraph VendorScope["Vendor-scoped (VendorId + VendorType)"]
    Vendor["Vendor\n(FlowerShop | FreelanceFlorist)"]
    Employee["Employee"]
    Bouquet["Bouquet\n(BouquetId, VaseId, Status, FreshnessScore)"]
    SmartVase["SmartVase\n(VaseId, Location, ConnectionStatus)"]
  end

  subgraph CustomerScope["Customer-scoped (CustomerId)"]
    Cart["Cart + CartItems\n(vendor-scoped constraint)"]
    Order["Order + OrderLines\n(multi-line, VendorId at header)"]
    Favourite["Favourite"]
    Subscription["Subscription"]
    Notification["Notification"]
    DeviceReg["DeviceRegistration"]
    SavedAddr["SavedAddress"]
    Feedback["BouquetFeedback"]
  end

  User --> Customer
  User --> Vendor
  Vendor --> Employee
  Vendor --> SmartVase
  Vendor --> Bouquet
  SmartVase --> Bouquet
  Customer --> Cart
  Customer --> Order
  Customer --> Favourite
  Customer --> Subscription
  Customer --> Notification
  Customer --> DeviceReg
  Customer --> SavedAddr
  Order --> Feedback

Sub-domain Mapping

Aggregate / Scope Business Sub-domain Notes
User (platform-scoped) Identity & Tenancy Polymorphic root linking to either a Customer or a Vendor; carries Role, VendorId?, CustomerId?
Vendor (FlowerShop / FreelanceFlorist) Vendor Management The multi-tenancy anchor; all vendor-scoped data is isolated by VendorId + VendorType
Employee Vendor Management Belongs to a vendor; used for role-based access within the vendor portal
SmartVase Vase & Bouquet Lifecycle / IoT Vendor-owned IoT device; tracks location, connection status, and the currently assigned bouquet
Bouquet Vase & Bouquet Lifecycle Vendor-scoped product; transitions through Available → Reserved → Sold / Expired
Customer (platform-scoped) Customer Purchasing Holds contact and address data; created when a User with Role = User is onboarded
Cart + CartItem Customer Purchasing Vendor-scoped (single-vendor constraint); DB-backed and device-persistent
Order + OrderLine Customer Purchasing / Payments Multi-line order with VendorId at the header; drives the payment and fulfilment state machine
Favourite / Subscription Customer Purchasing Preference data enabling notifications and personalised discovery
Notification / DeviceRegistration Customer Purchasing / IoT Push delivery infrastructure linking a customer’s device token to in-app and FCM notifications
SavedAddress Customer Purchasing Convenience data for repeat checkout flows
BouquetFeedback Customer Purchasing Post-purchase rating attached to an Order, scoped to a Bouquet

Data Lifecycle — Bouquet

stateDiagram-v2
  [*] --> Available : Vendor creates bouquet\n& assigns to vase
  Available --> Reserved : Customer adds to cart\n+ order created (15 min lock)
  Reserved --> Available : Reservation expires\nor order cancelled
  Reserved --> Sold : Payment confirmed
  Available --> Expired : Admin / vendor marks expired\nor TTL exceeded
  Sold --> [*]
  Expired --> [*]

Sub-domain Mapping — Bouquet Lifecycle

State Business Sub-domain Trigger
Available Vase & Bouquet Lifecycle Vendor creates a bouquet and assigns it to a vase
Reserved Customer Purchasing Customer adds bouquet to cart and order is created (15-minute lock)
Available (from Reserved) Customer Purchasing Reservation expires or order is cancelled — bouquet returns to market
Sold Payments Stripe webhook confirms payment; ownership transferred
Expired Vase & Bouquet Lifecycle Admin/vendor marks bouquet expired, or TTL exceeded

Data Lifecycle — Order

stateDiagram-v2
  [*] --> PendingPayment : POST /orders\nBouquets reserved
  PendingPayment --> PaymentProcessing : POST /orders/{id}/confirm-payment
  PaymentProcessing --> Confirmed : Stripe webhook success
  PaymentProcessing --> PaymentFailed : Stripe webhook failure
  PaymentFailed --> PendingPayment : Customer retries
  Confirmed --> ReadyForPickup : Vendor marks ready
  Confirmed --> OutForDelivery : Vendor dispatches
  ReadyForPickup --> Completed : Customer collects
  OutForDelivery --> Completed : Delivery confirmed
  Confirmed --> Cancelled : Admin / vendor cancels
  PendingPayment --> Cancelled : Reservation expires
  Completed --> [*]
  Cancelled --> [*]

Sub-domain Mapping — Order Lifecycle

State Business Sub-domain Trigger
PendingPayment Customer Purchasing POST /orders — bouquets reserved, awaiting payment
PaymentProcessing Payments Customer initiates Stripe checkout
Confirmed Payments Stripe webhook success — payment settled
PaymentFailed Payments Stripe webhook failure — customer can retry
ReadyForPickup / OutForDelivery Vendor Order Fulfilment Vendor updates order status via Vendor Portal
Completed Vendor Order Fulfilment Customer collects or delivery confirmed
Cancelled Customer Purchasing / Vendor Order Fulfilment Admin, vendor, or reservation timeout

Core Data Flows

flowchart LR
  subgraph Write["Write Path (Command)"]
    CH["Command Handler\n(MediatR)"]
    AG["Aggregate\n(domain logic)"]
    OB["outbox_messages\n(PostgreSQL)"]
    EV["Domain Event"]
    CH --> AG --> EV
    AG --> OB
  end

  subgraph Read["Read Path (Query)"]
    QH["Query Handler\n(MediatR)"]
    RC["Redis Cache\n(CachingBehaviour)"]
    PG["PostgreSQL\n(read replica)"]
    QH -->|check| RC
    RC -->|miss| PG
    PG --> RC
  end

  subgraph Dispatch["Async Dispatch"]
    OBD["OutboxDispatcherService\n(every 5s)"]
    BUS["InMemoryEventBus"]
    SIG["BouquetHub\n(SignalR)"]
    OBD -->|poll outbox| BUS --> SIG
  end

  Write --> Dispatch

Sub-domain Mapping — Data Flows

Path Business Sub-domain What moves
Write path (Command → Aggregate → Outbox) All sub-domains Business state mutations — bouquet creation, order placement, vase assignment
Read path (Query → Redis → PostgreSQL) Customer Purchasing / Vendor Management Bouquet search results, order history, vendor dashboards — served from cache on hot paths
Async dispatch (Outbox → EventBus → SignalR) IoT / Real-time + Customer Purchasing Freshness updates and order status events broadcast to connected clients

Information Store Responsibilities

Store Technology What it holds Retention / TTL
Primary DB PostgreSQL 15 All aggregates: Vendor, Bouquet, SmartVase, Order, Cart, Customer, etc. Permanent (soft deletes where applicable)
Query Cache Redis 7 Serialised query results keyed by {type}:{version}:{params} Per-query TTL; invalidated by version-key increment on writes
Token Blacklist Redis 7 Revoked JWT jti values Until JWT expiry
Idempotency Keys Redis 7 idempotency:{key}{status, body} 24 hours
Outbox PostgreSQL (table outbox_messages) Serialised domain events awaiting dispatch Until processed_at set; GC after 7 days
Geocoding Cache IMemoryCache (in-process) Nominatim OSM reverse-geocode results Short TTL, process-local only

Strongly-Typed ID Cross-Reference

flowchart LR
  VendorId["VendorId (Guid)"] -->|scopes| SmartVase2["SmartVase"]
  VendorId -->|scopes| Bouquet2["Bouquet"]
  VendorId -->|scopes| Employee2["Employee"]
  VendorId -->|header of| Order2["Order"]
  VendorId -->|header of| Cart2["Cart"]

  BouquetId["BouquetId (Guid)"] --> OrderLine2["OrderLine"]
  BouquetId --> Favourite2["Favourite"]
  BouquetId --> CartItem2["CartItem"]
  BouquetId --> Feedback2["BouquetFeedback"]

  CustomerId["CustomerId (Guid)"] --> Order3["Order"]
  CustomerId --> Cart3["Cart"]
  CustomerId --> Favourite3["Favourite"]
  CustomerId --> Subscription2["Subscription"]
  CustomerId --> Notification2["Notification"]

  VaseId["VaseId (Guid)"] --> Bouquet3["Bouquet (AssignedVaseId)"]
  VaseId --> CartItem3["CartItem (VaseId)"]