Skip to main content

Asset Persistence Architecture

Summary

Source-backed description of how an Asset operation travels from an HTTP endpoint down to PostgreSQL — through application commands and queries, the repository and unit of work, AssetDbContext, the Asset aggregate, the audit and timeline writers, and outbox staging — and where external HTTP calls to the Workflow Service and Document Service sit relative to the local database transaction.

Audience

Backend engineers, database reviewers, architects, QA, support, operations, security reviewers, and product owners.

Overview

The Asset Service owns a dedicated logical schema in its own PostgreSQL database. Write requests enter through minimal API endpoints, are handled by application commands, load and mutate the Asset aggregate in memory, stage evidence and integration events, and are committed through a single SaveChangesAsync on AssetDbContext. Read requests are served by application queries over the same DbContext.

The important architectural boundary is that domain mutation, EF change tracking, evidence writes, and outbox writes are all part of one local database transaction, while calls to the Workflow Service and Document Service are made over HTTP outside that transaction.

Confirmed persistence behavior

The verified persistence flow, in order:

  1. Minimal API endpoints receive the request and resolve the tenant context (ITenantContext). Write paths call RequireTenantId().
  2. Application commands / queries (AssetCommands, AssetQueries, AssetCompatService) orchestrate the operation. Commands load the aggregate through the repository; queries read through the DbContext.
  3. Repository + unit of work (Domain/Repositories.cs) expose aggregate load (GetAsync, GetByCompatIdAsync, both eager-loading history and documents), the code-existence check (CodeExistsAsync), and SaveChangesAsync.
  4. AssetDbContext tracks changes to the aggregate and its children and provides the object sets. See DbContext and Object Sets.
  5. Asset aggregate performs in-memory domain mutation (status/condition/assignee changes, history append) and records domain events on itself.
  6. Audit and timeline writers (IAuditWriter, ITimelineWriter) stage evidence rows into the same unit of work.
  7. Outbox staging happens inside the SaveChangesAsync override: DispatchDomainEventsToOutbox() maps the aggregate's domain events to contract integration events, runs the EnsureComplete completeness guard, and writes outbox rows — all before the base save, in the same transaction.
  8. PostgreSQL provider (Npgsql, EF Core 8) commits the aggregate row, history rows, document-reference rows, audit rows, timeline rows, and outbox rows together in one implicit local transaction.

External HTTP calls sit outside this transaction:

  • Workflow Service start/verify calls are made from the application layer, not from within the DbContext transaction. On the create-request path the asset is saved first, the workflow call is made next, and a second save attaches the returned instance id.
  • Document Service store and download calls are made over HTTP; the store call happens before the document-reference row is saved. File bytes are never held in the Asset database — only a reference is persisted.

Because these calls are outside the local transaction, there is no distributed transaction and no automatic compensation if an external call succeeds while a later local save fails. Transaction and failure semantics are detailed in Transactions and Save Boundaries.

Distinct persistence stages to keep separate:

  • In-memory domain mutation — the aggregate changes state; nothing has touched the database yet.
  • EF change tracking — the DbContext records inserts/updates for the aggregate, its children, and staged evidence.
  • Save boundarySaveChangesAsync dispatches domain events to the outbox, then calls base save; one local transaction commits everything staged.
  • Outbox persistence in the same transaction — integration events are durable if and only if the local save commits.
  • External calls outside the transaction — Workflow and Document Service HTTP calls, before or after persistence, with no shared atomicity.

The outbox relay (OutboxRelayHostedService) is a separate background process that polls staged outbox rows and publishes them; it does not participate in the request-time transaction.

Classification

Implemented single-transaction persistence with outbox staging; external integration is Transitional and non-transactional by design.

Requires confirmation

Production migration ownership, broker availability for the outbox relay, and operational handling of the external-call-succeeds-while-local-save-fails window require confirmation.

Diagram

See Also

Keywords

  • Asset persistence
  • Persistence Architecture
  • Draft database documentation

Source References

  • microservices/src/asset-service/Infrastructure/AssetDbContext.cs
  • microservices/src/asset-service/Application/Commands/AssetCommands.cs
  • microservices/src/asset-service/Domain/Repositories.cs
  • microservices/src/asset-service/Messaging/OutboxRelayHostedService.cs

Revision Information

  • Status: Draft
  • Last reviewed: 2026-07-17
  • Review cycle: Quarterly