Skip to main content

Developer Documentation

Welcome to the RoundTrip developer documentation. This section is the primary reference for all engineering work on the RoundTrip platform — covering architecture, coding standards, patterns, tooling, and day-to-day development workflow.

Before writing a single line of code, every developer on the team should read this introduction in full. RoundTrip is built on architectural patterns — Clean Architecture, Domain-Driven Design, and the Mediator pattern — that may be unfamiliar if you are coming from a more traditional layered or MVC codebase. Taking the time to understand these patterns upfront will save you significant frustration later.

:::warning Read This First RoundTrip uses several patterns that behave differently from what most developers expect. The Gotchas & Things to Know section below documents the most common mistakes. Read it before touching the codebase. :::


The Stack

LayerTechnologyNotes
API Runtime.NET 10dotnet run --project src/RoundTrip.API.Web --launch-profile http (runs on http://localhost:5000)
API FrameworkFastEndpointsReplaces controllers — one endpoint class per route
MediatorCyrus (martinothammar source generator)Compile-time generated — see Cyrus docs
ORMEF CoreUsed for migrations and writes only
Query LayerDapperUsed for all reads — raw SQL, fast, explicit
Background JobsHangfireScheduled and enqueued jobs — SQL Server backed
Real-timeSignalRNotifications pushed to connected clients
AuthMicrosoft Entra External ID CIAM + MSALJWT tokens validated on every request
FrontendReact 19, Vite, TypeScript, Tailwind v4PWA-capable
State / DataTanStack Query + ZustandServer state vs UI state — keep them separate
Issue TrackingLinearAll work tracked as TRA-NNN tickets

Architectural Overview

RoundTrip is built on three foundational patterns that work together. Understanding all three is required — they are not optional conventions, they are the architecture.

1. Clean Architecture

Clean Architecture organizes the codebase into concentric layers where dependencies only point inward. The innermost layers have no knowledge of the outer layers.

┌─────────────────────────────────────────┐
│ API / Web Layer │ ← FastEndpoints, SignalR, Hangfire
├─────────────────────────────────────────┤
│ Infrastructure Layer │ ← EF Core, Dapper, SendGrid, Graph API
├─────────────────────────────────────────┤
│ Use Cases Layer │ ← Commands, Queries, Handlers
├─────────────────────────────────────────┤
│ Domain / Core │ ← Entities, Value Objects, Domain Events
└─────────────────────────────────────────┘

The golden rule: inner layers never reference outer layers. The Domain layer has zero knowledge of EF Core, HTTP, or any infrastructure concern. If you find yourself importing an infrastructure namespace into a domain entity, stop — something is wrong.

See Clean Architecture for a full deep-dive.

2. Domain-Driven Design (DDD)

DoundTrip models the business domain explicitly in code. The domain layer contains rich entities with behavior — not anemic data bags. Key concepts you will encounter:

  • Aggregates — clusters of entities treated as a single unit (e.g. Ticket, Invoice, Client)
  • Value Objects — immutable types defined by their value, not identity (e.g. ServiceAddress, Money) — implemented as readonly record struct
  • Domain Events — things that happened in the domain, raised by aggregates and handled asynchronously
  • Repositories — abstraction over persistence, defined in Core, implemented in Infrastructure

See Domain-Driven Design for a full deep-dive.

3. Mediator Pattern (Cyrus)

All use cases — every command and query — flow through the Mediator. An endpoint never contains business logic directly. It sends a command or query to the Mediator and returns the result.

Request → FastEndpoint → IMediator.Send(command) → Handler → Response

RoundTrip uses Cyrus (martinothammar's source generator), which generates the mediator dispatch at compile time rather than using reflection. This means:

  • Handlers must implement IRequestHandler<TRequest, TResponse>
  • Requests must implement IRequest<TResponse>
  • Pipeline behaviors only intercept IRequest<>not IQuery<>

See Cyrus Mediator for full documentation.


Why Dapper for Reads?

If you are used to using EF Core for everything, this may feel unfamiliar. RoundTrip uses a split persistence strategy:

  • EF Core handles writes, migrations, and relationship tracking
  • Dapper handles all reads via raw SQL

This is an intentional performance and clarity decision. Read queries in field service management can be complex — dispatch board data, reporting, service history — and raw SQL gives you full control over what hits the database. EF Core's query translation can produce inefficient SQL for complex joins that Dapper handles cleanly.

In practice this means:

  • Query handlers use IDbConnectionFactory to get a SqlConnection and execute SQL directly
  • You write the SQL — what you write is what runs
  • No lazy loading surprises, no N+1 query traps hidden behind navigation properties

See Backend Coding for patterns and examples.


Gotchas & Things to Know

These are the most common mistakes developers make when first working in this codebase. Read them now and save yourself hours of debugging.

:::danger Cyrus Mediator — Pipeline Behaviors Pipeline behaviors (like TransactionBehavior, TenantScopingBehavior) only intercept types that implement IRequest<>. They do not intercept IQuery<>. All command and query records must implement IRequest<TResponse> and handlers must implement IRequestHandler<,>. If your handler is not being intercepted by behaviors, this is why. :::

:::danger Domain Events — Do Not Use IMediator.Publish Domain events are dispatched via IDomainEventPublisher, not IMediator.Publish(). The Cyrus source generator does not generate notification handler dispatch. Domain event handlers are registered as IDomainEventHandler<T> in MediatorConfig.cs. Never register them as INotificationHandler<T>. :::

:::danger EF Core Migrations — Never Run at Startup EF Core migrations must never run automatically at application startup. This causes startup timeouts in production. Migrations are created with dotnet ef migrations add and applied manually. See Backend Coding for the correct process. :::

:::danger SQL Server Sequences and Transactions CREATE SEQUENCE and other DDL statements in SQL Server are transactional and will roll back if executed inside an open EF Core transaction (via TransactionBehavior). DDL must be run on a separate ADO.NET connection opened outside any active transaction scope. :::

:::caution TenantScopingBehavior — IsInitialised Guard TenantScopingBehavior has an IsInitialised guard that skips re-initialisation on subsequent calls within the same scope. This is intentional. Do not remove it or work around it. :::

:::caution ServiceAddress.Create() Normalizes State and Country ServiceAddress.Create() automatically normalizes State and Country to uppercase. Do not pre-format these values before passing them in — you will end up double-processing them. :::

:::caution Key Vault Secret Changes Require Stop/Start When a Key Vault secret reference is added or updated in App Service configuration, the App Service must be fully stopped then started — not restarted. A restart does not flush the Key Vault reference cache. This applies in both production and dev environments. :::


Development Workflow

All work on RoundTrip follows a consistent workflow. Deviating from this causes pipeline issues and merge conflicts.

Branch Naming

feature/TRA-NNN-short-description
fix/TRA-NNN-short-description

Commit Conventions

feat(TRA-NNN): description of what was added
fix(TRA-NNN): description of what was fixed
test(TRA-NNN): tests added for ticket
chore: dependency update or tooling change
refactor(TRA-NNN): internal restructure, no behavior change

PR Flow

feature/TRA-NNN → development → main
  • PRs are merged sequentially — never merge multiple feature branches simultaneously
  • Tests must be written in the same session as the ticket before it is closed
  • Every ticket must be updated in Linear before closing

Running the API Locally

dotnet run --project src/RoundTrip.API.Web --launch-profile http
# Runs at http://localhost:5000

Work through the developer documentation in this order:

  1. Frontend Structure — how the React app is organized
  2. UI Patterns — existing components and how to use them
  3. Frontend Coding — standards and conventions
  4. Backend Coding — API patterns, handler structure, Dapper usage
  5. Cyrus Mediator — deep dive on the mediator pattern
  6. Azure CLI Reference — common Azure CLI commands for the team

When those are solid, read the architecture deep-dives:


Have a question not answered here? Check Notes for architecture decisions, or ask in the team channel.