Whetstone.
Persistence as dumb IOThe adapter, and DBO is not the VO
Module 7 · Lesson 112 min

The adapter, and DBO is not the VO

Persistence is the outer layer, and its whole job is boring on purpose. An adapter implements a port and does IO: it maps the domain object to a database row, and saves. It does not validate, calculate, or call domain factories. Boring is the goal; interesting persistence code usually means domain logic leaked outward.

The adapter implements the port (persistence/FlightNotes/FlightNoteRepository.cs)
public sealed class FlightNoteRepository : IFlightNoteRepository
{
    private readonly FusionDbContext _dbContext;

    public FlightNoteRepository(FusionDbContext dbContext) => _dbContext = dbContext;

    [Trace]
    public async Task SaveMany(IReadOnlyCollection<FlightNoteVo> notes, CancellationToken cancellationToken)
    {
        _dbContext.FlightNotes.AddRange(notes.Select(FlightNote.From));
        await _dbContext.SaveChangesAsync(cancellationToken);
    }
}

The row it saves is a DBO (a database object), and this is a distinction Nest can blur: the DBO is NOT the value object. FlightNoteVo is the domain value object (behaviour, invariants, private ctor). FlightNote is a persistence record: EF attributes, an Id, audit columns, public setters. A mapper, FlightNote.From(vo), copies VO to DBO.

The DBO maps FROM the VO, no validation (persistence/Dbos/FlightNote.cs)
public static FlightNote From(FlightNoteVo note) =>
    new()
    {
        Id = MonotoneUuidV7.Next(),
        EjdpUniqueFlightKey = note.EjdpUniqueFlightKey,
        Body = note.Body
        // Audit fields intentionally omitted, filled by Postgres on INSERT.
    };
Practice

Try it yourself

Do

Trace one note through the layers

Walk a single note from request to row and name the boundary each hop crosses. Use the real files.

  • Start in application/flight-notes/SaveFlightNote.cs: the use case builds a FlightNoteVo via Create (domain)
  • It hands the VO to IFlightNoteRepository.SaveMany — a port it depends on but doesn't implement
  • Jump to persistence/FlightNotes/FlightNoteRepository.cs: the adapter implements that port and maps VO → DBO via FlightNote.From
  • Confirm FlightNote.From copies fields but never calls FlightNoteVo.Create — the domain factory stays in the domain (hard rule 5)
Done whenYou traced request → VO (domain) → port (application) → adapter + DBO (persistence), and confirmed persistence maps but never runs the domain Create factory.
Recall

VO versus DBO

Two representations, know the difference.

What's the difference between FlightNoteVo and the FlightNote DBO, and what must the DBO's From mapper NOT do?

Reveal answer

FlightNoteVo is the domain value object: private ctor, Create factory, invariants, no persistence concerns. FlightNote is the DBO: an EF record with Id, audit columns, and public setters. From(vo) copies the VO's fields to a new DBO and must NOT call the domain Create factory or run any domain behaviour, because persistence never invokes domain logic (hard rule 5).

Quiz

What the adapter may do

Which of these is legitimate work for a persistence adapter?

  1. ACall FlightNoteVo.Create to validate before saving
  2. BRecalculate the disruption cost before writing
  3. CMap the VO to a DBO and call SaveChangesAsync
  4. DThrow a BadRequest if the body is too long
Show answer

Correct answer: C — Map the VO to a DBO and call SaveChangesAsync

Mapping VO → DBO and saving is the adapter's whole job. Validation (Create), calculation, and business errors all belong in the domain/application; persistence must never invoke domain behaviour (hard rule 5).

Sign in to track your progress →