CQRS: reads and writes split
Fusion splits reading from writing at the port level. This is CQRS (command-query responsibility segregation), and here it’s a naming and interface convention more than a grand architecture. Writes and entity-loading go on a Repository; reads go on a QueryStore. The split makes it obvious at a glance which methods have side effects.
// Command side: writes
public interface IFlightNoteRepository
{
Task SaveMany(IReadOnlyCollection<FlightNoteVo> notes, CancellationToken cancellationToken);
}
// Read side: projections for list views
public interface IFlightNoteQueryStore
{
Task<IReadOnlyDictionary<string, string>> GetLatestNoteByFlight(
IEnumerable<string> ejdpUniqueFlightKeys, CancellationToken cancellationToken);
}Two related conventions ride along. Command-side repositories return aggregates, not DTOs (D4); a read-side projection like a summary DTO belongs on the query store instead. And a warning against your Nest instinct: don’t invent a new DTO for a concept that already has a representation (anti-pattern #13). Each representation must earn a unique use case, or it’s just another sync point to break.
Try it yourself
Split a read out of a repository
Refactor a Nest-style all-in-one repo into the CQRS shape.
- Take an
INoteRepositorythat has bothSaveNote(...)andGetNotesForFlight(...) - Move the read onto a new
INoteQueryStoreasGetNotesForFlight(...), returning a projection - Leave
SaveMany(...)onINoteRepository - Confirm a caller that only lists notes now depends on the QueryStore and can't accidentally call a write
Which port gets the method
You need a method that loads a flight aggregate to run an action on it. Which port and which name prefix?
Show answer
Correct answer: B — Repository, Load*
Loading an entity FOR AN OPERATION is the command side: a Repository with a Load* name. Get* on a QueryStore is read-only projection. Mixing them up (Load* on a QueryStore, Get* on a Repository) is exactly what anti-pattern #6 flags.