Clean architecture

Infrastructure

A request-scoped store, a row type, and a mapper. This is where Session would sit.

clean-architectureserverpodfeature-first

The adapter lives at lib/src/features/notes/infra/, beside the entity it stores. That proximity is the layout. The import still points inward: the impl imports the domain port, and the domain does not import the impl.

NoteRow is the persistence model. It stores createdAtMicros instead of a DateTime, so the row is not a note with a different name. NoteMapper.toRow and toNote are the only functions that see both. toNote rebuilds with the Note constructor. Rows here were written by save after Note.create, so the round trip keeps a title the domain already accepted. A store that can hold a row create would refuse should follow Infrastructure: run the invariant on load, or fail the load.

NoteStore is a map of rows. NoteRepositoryImpl takes that store in its constructor, the way a Serverpod repository impl takes a Session. The composition root builds a new store per graph. Do not cache the impl on a process-wide singleton. The next request would share it.

dart
class NoteRepositoryImpl implements NoteRepository {
  NoteRepositoryImpl(this._store, {NoteMapper? mapper})
    : _mapper = mapper ?? const NoteMapper();

  final NoteStore _store;
  final NoteMapper _mapper;
}

In a generated Serverpod server, replace NoteStore with Session, put the table in a serverOnly model, and map uuid columns at this boundary. The port in the domain stays save and findAll. The file is lib/src/features/notes/infra/note_repository_impl.dart.

This layer imports the domain. It does not import the use case or the endpoint.

Next: Presentation.