One request
Save a note from the wire payload, through the use case and the domain, to the row that is stored.
This is SaveNote from the caller's payload to the row, and back. The same steps apply in a layer-first tree and a feature-first tree. Only the paths change.
The call
A client submits:
{ "title": "Market", "body": "Buy oat milk" }That JSON is the wire model. It has no id and no timestamp.
Presentation
The entry point does not construct a repository. It maps, calls, and maps again.
save(request):
try:
command = SaveNoteCommand(
title: request.payload.title,
body: request.payload.body,
)
result = saveNote.execute(command)
return 201, SaveNoteResponse(
id: result.note.id,
title: result.note.title,
body: result.note.body,
createdAt: result.note.createdAt,
)
catch InvalidTitle:
return 400, field error on title
catch storage failure:
return 500saveNote was built by the composition root for this request.
Application
SaveNote.execute(command):
note = Note.create(command.title, command.body, clock.now())
saved = notes.save(note)
return SaveNoteResult(saved)No status code, no JSON, no table name. notes is a NoteRepository. In production it is SqlNoteRepository. In a unit test it is a fake. This function does not know which.
Domain
Note.create(title, body, now):
if title is blank:
fail InvalidTitle
return Note(newId(), title, body, now)For "Market" this returns a note. For "" it fails, notes.save is never called, and presentation maps InvalidTitle to the field error.
Infrastructure
SqlNoteRepository.save(note):
insert into notes (
id, title, body, created_at
) values (
note.id, note.title, note.body, note.createdAt
)
return noteThe insert is the persistence model. If the driver reports a timeout, that failure propagates as an infrastructure failure. The repository does not translate it into InvalidTitle.
What came back
201
{
"id": "…",
"title": "Market",
"body": "Buy oat milk",
"createdAt": "…"
}The response is a fourth shape only in the sense that it is the wire model on the way out. It is still presentation. The entity gained an id and a timestamp the client did not send. The mapper is the only place that copies those fields onto the response.
A blank title
{ "title": "", "body": "Buy oat milk" }Note.create fails. The use case ends. The repository is not called. The client receives a field error. The table is unchanged. That is the dependency rule at runtime: the inner ring can refuse the outer ring's data, and the outer ring cannot write anyway, because the write lives behind a port the use case never reached.
Where the files sit
Layer-first, the pieces are presentation/notes, application/notes, domain/notes, infrastructure/notes, and composition. Feature-first, the same five roles sit under features/notes/, and composition stays shared. Hybrid, the use case stays in horizontal application/notes even when the entry point lives in presentation/notes. Moving the use case into the feature folder would make the workflow depend on the edge.