Skip to main content

ROUNDTRIP SOLUTION ARCHITECTURE DOCUMENT

Version: 1.1
Date: August 2026
Author: Pete Carroll, Founder — Traxs Group LLC
Status: Draft


01. Introduction

Purpose

This Solution Architecture Document (SAD) describes the technical architecture of Roundtrip — a multi-tenant Software-as-a-Service (SaaS) field service management platform developed by Traxs Company. It defines the system's structural decomposition, key architectural decisions, component interactions, data flows, deployment topology, and quality attribute strategies. This document is intended for software architects, developers, DevOps engineers, and technical stakeholders involved in the design and implementation of Roundtrip.

Scope

This document covers the complete Roundtrip platform, including: • The ASP.NET Core 10 backend API and its internal architecture • The React 19 frontend application for dispatchers and office staff • The multi-tenant data isolation strategy • The AI-powered route optimization pipeline (n8n + Ollama) • Real-time dispatch notifications via SignalR • Background job processing via Hangfire • Identity and access management via Entra ID • Deployment and infrastructure topology • Integration with external services (mapping, SMS, email, PDF generation)

Background

Roundtrip addresses the operational needs of field service businesses — HVAC, plumbing, auto service, computer repair, and similar industries. These businesses share common challenges: managing technician schedules, routing efficiently across multiple daily service calls, tracking parts inventory, generating invoices and quotes, and maintaining service history per client. Roundtrip is built as a multi-tenant SaaS platform, meaning a single deployed instance serves multiple business customers (tenants) with complete data isolation. This architecture reduces operational cost, simplifies maintenance, and enables rapid tenant onboarding.

Architectural Goals

GoalDescriptionPriority
Multi-TenancyComplete data isolation between tenants at every layer — database, application, and identityCritical
ScalabilityHorizontal scaling of API servers without architectural changes; tenant-specific database scalingHigh
ReliabilityHorizontal scaling of API servers without architectural changes; tenant-specific database scalingHigh
Data SovereigntyAI inference runs locally via Ollama — tenant data never leaves the infrastructureHigh
Developer ProductivityClean Architecture, CQRS, and DDD patterns enable rapid feature development without regressionsMedium
ObservabilityEvery request, job, and AI call is traceable via OpenTelemetry with tenant contextMedium
ExtensibilityAPI-first design allows third-party integrations (accounting, CRM) without core changesMedium

02. Architectural Overview

Architectural Style

Roundtrip follows a Layered Monolith architecture for the initial release, structured internally according to Clean Architecture principles. This is a deliberate choice — a well-structured monolith is faster to develop, easier to deploy, and cheaper to operate than microservices at early scale. The internal boundaries are designed to allow extraction into microservices if specific domains require independent scaling in the future.

Architecture Decision: Modular Monolith over Microservices

Microservices introduce significant operational complexity: service discovery, distributed transactions, inter-service authentication, network latency, and independent deployment pipelines. For a startup SaaS product, this overhead is premature. Roundtrip's Clean Architecture with CQRS creates the same internal boundaries that microservices would provide, but without the distributed systems complexity. The Tickets, Inventory, Routing, Billing, and Identity modules are logically isolated and can be extracted to services when justified by scale.

High-Level System Diagram

03 — APPLICATION ARCHITECTURE

Clean Architecture

Layer Structure

Roundtrip's backend is structured according to Clean Architecture (also known as Onion Architecture). Dependencies flow strictly inward — outer layers depend on inner layers, never the reverse. This ensures the domain model is completely independent of infrastructure concerns like databases, ORMs, and external APIs.

Layer Responsibilities

LayerProject NameResponsibilitiesKey Technologies
DomainRoundtrip.CoreEntities, Value Objects, Domain Events, Aggregate Roots, Domain InterfacesPure C# — no external dependencies
ApplicationRoundtrip.UseCasesCQRS Handlers, Use Cases, Application Services, Validation Pipeline, DTOsMediator, FluentValidation, Ardalis.Specification
InfrastructureRoundtrip.InfrastructureEF Core DbContext, Repositories, External API Clients, Background Jobs, Email/SMSEF Core, Dapper, Redis, Hangfire, SendGrid, Twilio
APIRoundtrip.ApiFastEndpoints, SignalR Hubs, Authentication Middleware, Tenant Resolution, OpenAPIASP.NET Core 9, FastEndpoints, Entra ID, JWT, SignalR
TestsRoundtrip.Tests.*Unit tests (domain/application), Integration testsxUnit, FluentAssertions, Testcontainers, NetArchTest, (Testcontainers), Architecture tests (NetArchTest)

Architecture Rules (Enforced by NetArchTest)

• Domain layer MUST NOT reference any external NuGet packages (pure C#) • Application layer MUST NOT reference Infrastructure or API projects • Infrastructure MUST NOT reference API project • All EF Core references MUST be in Infrastructure layer only • All Mediator handlers MUST be in Application layer • FastEndpoints MUST only reference Application DTOs, not Domain entities directly

04 — CQRS & REQUEST PIPELINE

CQRS and Request Pipeline

CQRS Pattern

Roundtrip implements Command Query Responsibility Segregation (CQRS) via Mediator. All state-changing operations are Commands; all read operations are Queries. This separation enables independent optimization of read and write paths, and ensures handlers remain focused and testable.

Pipeline Behaviors

BehaviorApplies ToResponsibility
LoggingBehaviorAll requestsLogs request type, tenant ID, duration, and outcome to Serilog with structured fields
ValidationBehaviorAll requestsExecutes FluentValidation validators; returns 400 with problem details on failure without reaching handler
TenantScopingBehaviorAll requestsResolves TenantContext from JWT claim and injects into scoped services; prevents handler executing without valid tenant
CachingBehaviorQueries onlyChecks Redis cache for query result; on miss, executes handler and populates cache with tenant-scoped key
TransactionBehaviorCommands onlyWraps handler execution in EF Core transaction; rolls back on exception; raises domain events post-commit

Result Pattern

All handlers return Result<T> rather than throwing exceptions. This provides explicit success/failure signalling without exception-driven flow control, and maps cleanly to HTTP problem details responses in FastEndpoints.

// Command Handler return types
Result<CreateTicketResponse> // Success with data
Result.Failure("Technician not found in this tenant") // Domain error
Result.NotFound() // Entity not found
Result.Unauthorized() // Permission denied

// FastEndpoint mapping
var result = await mediator.Send(command);
if (result.IsFailure) return TypedResults.Problem(result.Error);
return TypedResults.Ok(result.Value);

05 — MULTI-TENANCY ARCHITECTURE

Multi-Tenancy Architecture

Tenancy Model

Roundtrip implements a hybrid tenancy model. The choice of isolation level is driven by tenant tier, data volume, and compliance requirements. All tenants share the same application codebase and API infrastructure; only the data layer varies.

TierIsolation ModelDatabase StrategyTarget Customer
BasicShared SchemaSingle DB, TenantId column on every table, EF Core Global Query FilterSmall businesses (1–3 technicians)
StandardShared SchemaSingle DB, TenantId column on every table, EF Core Global Query FilterSmall businesses (1–10 technicians)
ProfessionalSchema IsolationSingle DB server, separate schema per tenant (tenant_abc.Tickets)Mid-size businesses (10–50 technicians)

Tenant Identification

Each incoming HTTP request is resolved to a tenant using the following strategy, evaluated in priority order: • JWT Claim — tid (tenant ID) claim in the Keycloak-issued access token (primary method) • Subdomain — tenant.roundtrip.app resolves to tenant slug, used for unauthenticated routes (login page) • Request Header — X-Tenant-Id header (for API-to-API calls and webhook receivers)

// Tenant Resolution Middleware
public class TenantResolutionMiddleware
{
public async Task InvokeAsync(HttpContext context)
{
// 1. Try JWT claim first
var tenantId = context.User.FindFirst("tid")?.Value
// 2. Fall back to subdomain
?? ExtractFromSubdomain(context.Request.Host)
// 3. Fall back to header
?? context.Request.Headers["X-Tenant-Id"].FirstOrDefault();

if (tenantId == null) { context.Response.StatusCode = 401; return; }

var tenant = await _tenantRegistry.GetAsync(tenantId);
context.Items["TenantContext"] = tenant;
await _next(context);
}
}

EF Core Global Query Filter

The TenantId filter is applied globally in OnModelCreating, ensuring every EF Core query automatically includes WHERE TenantId = @currentTenantId without any handler needing to remember to filter manually. This provides defence-in-depth against data leakage.

// ApplicationDbContext.cs
protected override void OnModelCreating(ModelBuilder builder)
{
// Apply to every entity implementing ITenantEntity
foreach (var entityType in builder.Model.GetEntityTypes()
.Where(e => typeof(ITenantEntity).IsAssignableFrom(e.ClrType)))
{
builder.Entity(entityType.ClrType)
.HasQueryFilter(e => EF.Property<Guid>(e, "TenantId")
== _tenantContext.TenantId);
}
}

Entra ID

description of Entra ID coming soon

06 — DOMAIN MODEL

Domain Model Overview

Core Aggregates

The Roundtrip domain is organized into the following bounded contexts and aggregate roots. Each aggregate is responsible for maintaining its own invariants and raising domain events when significant state changes occur.

Aggregate RootBounded ContextKey Entities / Value ObjectsDomain Events
ServiceTicketTicketsTicketId, ClientId, TechnicianId, ServiceAddress (VO), TicketStatus (Enum), PartUsages, NotesTicketCreated, TicketAssigned, TicketCompleted, TicketCancelled
ClientClientsClientId, ContactInfo (VO), ServiceAddress (VO), ServiceHistoryClientCreated, ClientUpdated
TechnicianUsersTechnicianId, UserId, Skills, CurrentLocation (VO), ScheduleTechnicianAssigned, LocationUpdated
InventoryItemInventoryItemId, SKU, PartName, Quantity, ReorderThreshold, LocationStockLow, ItemConsumed, ItemRestocked
InvoiceBillingInvoiceId, TicketId, LineItems, TotalAmount, InvoiceStatusInvoiceGenerated, InvoiceSent, PaymentReceived
TechnicianRouteRouteId, TechnicianId, Date, Stops (VO list), OptimizedOrderRouteOptimized, RouteUpdated
TenantIdentityTenantId, TenantSlug, Plan, Settings, ConnectionString (Enterprise)TenantProvisioned, PlanChanged

Key Value Objects

Value ObjectPropertiesInvariants
ServiceAddressStreet, City, State, PostalCode, Country, Latitude, LongitudePostalCode must match City/State. Lat/Lng populated on geocoding. Cannot be partially constructed.
ContactInfoFirstName, LastName, Email, PrimaryPhone, SecondaryPhoneEmail must be valid format. PrimaryPhone required. At least one contact method required.
MoneyAmount (decimal), Currency (ISO 4217)Amount cannot be negative. Currency must be valid ISO code. Arithmetic preserves currency.
TicketStatusEnum: Open, Assigned, InProgress, OnHold, Completed, CancelledStatus transitions follow defined state machine. Completed/Cancelled are terminal states.
GPS CoordinateLatitude, LongitudeLat: -90 to 90. Lng: -180 to 180. Both required together.

Ticket State Machine

07 — API DESIGN

API Design

API Style & Conventions

Roundtrip exposes a RESTful HTTP API built with FastEndpoints (REPR pattern). All endpoints are versioned, tenant-scoped, and return RFC 7807 Problem Details on error. The API is documented via Scalar (OpenAPI 3.0).

ConventionRule
Base URLhttps://api.roundtrip.app/v1/ (or https://{tenant}.roundtrip.app/api/v1/)
VersioningURL path versioning: /v1/, /v2/ — new major version on breaking changes only
AuthenticationBearer JWT (Entra ID access token) in Authorization header
Tenant ScopingResolved from JWT tid claim; all resources automatically scoped to tenant
Response Formatapplication/json — all responses. Problem Details (RFC 7807) for errors
PaginationCursor-based pagination for list endpoints: ?cursor=&limit=50
FilteringQuery string parameters: ?status=open&technicianId=&from=&to=
HTTP MethodsGET (read), POST (create), PUT (full update), PATCH (partial update), DELETE
Status Codes200 OK, 201 Created, 204 No Content, 400 Bad Request, 401, 403, 404, 409 Conflict, 422 Unprocessable

Key API Endpoints

MethodEndpointDescriptionAuth
GET/v1/ticketsList tickets for tenant (paginated, filterable)Dispatcher+
POST/v1/ticketsCreate new service ticketDispatcher+
GET/v1/tickets/{id}Get ticket by ID with full detailsAll roles
PATCH/v1/tickets/{id}/status Update ticket status (state machine enforced)Technician+
POST/v1/tickets/{id}/partsRecord part usage on ticket (photo + quantity)Technician
GET/v1/clientsList clients with service history summaryDispatcher+
POST/v1/clientsCreate new client recordDispatcher+
GET/v1/clients/{id}/history Full service history for a clientDispatcher+
GET/v1/routing/{technicianId}/todayGet optimized route for technician todayTechnician, Dispatcher
POST/v1/routing/optimize`Trigger route optimization (calls n8n/Ollama)Dispatcher
GET/v1/inventoryList inventory items with stock levelsAll roles
PATCH/v1/inventory/{id}/quantityAdjust stock quantityTenantAdmin, Dispatcher
GET/v1/inventory/low-stockItems below reorder thresholdDispatcher+
POST/v1/invoicesGenerate invoice from completed ticketBillingStaff+
POST/v1/invoices/{id}/sendSend invoice via email/SMSBillingStaff+
GET/v1/invoices/{id}/pdfDownload invoice as PDFBillingStaff+
GET/v1/techniciansList technicians with current statusDispatcher+
PATCH/v1/technicians/{id}/locationUpdate technician GPS locationTechnician (self only)

08 — REAL-TIME & BACKGROUND PROCESSING

Real-Time and Background Processing

SignalR Real-Time Architecture

Roundtrip uses SignalR for real-time push to the dispatcher dashboard and technician PWA. All SignalR groups are tenant-scoped to prevent cross-tenant data leakage.

SignalR Group Dispatchers

SignalR Group Technicians

SignalR Backplane

Hangfire Background Jobs

Roundtrip uses Hangfire for all background processing. Jobs store in SQL Server (shared with application data), providing reliable persistence across restarts. All jobs are tenant-aware via injected TenantContext.

JobTypeTriggerRetry Policy
SendInvoiceEmailFire-and-forgetInvoice.Send command via IMediator3 retries, 5min exponential backoff
SendInvoiceSMSFire-and-forgetInvoice.Send command via IMediator3 retries, 5min exponential backoff
GenerateInvoicePdfFire-and-forgetTicket completed domain event3 retries, immediate
OptimizeRoutesRecurringDaily at 5:00 AM per tenant timezone2 retries, alert on final failure
LowStockAlertRecurringEvery 6 hours per tenant2 retries
TenantDailyReportRecurringDaily at 8:00 AM per tenant timezone2 retries
PurgeExpiredCacheRecurringDaily at 2:00 AM (system)1 retry
TenantOnboardingContinuation chainTenant registration: provision DB → create realm → seed data → send welcomePer-step retries

09 — AI & AUTOMATION ARCHITECTURE

AI and Automation Architecture

Route Optimization Pipeline

Roundtrip's route optimization runs via an n8n workflow triggered daily by Hangfire. The workflow calls Ollama's local LLM to produce an optimized stop order for each technician, then writes the result back to the Roundtrip API. No tenant data leaves the infrastructure.

This section is under review

We need to evaluate which AI infrastructure to implement for the Traxs One Project and not just RoundTrip. The platforms we will evaluate are:

  1. n8n -- Ollama self contained AI platform
  2. Microsoft Foundry - Azure Service
  3. Microsoft Foundry - Local

n8n

AI agents and workflows you can see and control

Build visually, go deep with code, connect to anything. Every step of your agents' reasoning, traceable on the canvas. Deploy on your infrastructure or ours.

Microsoft Foundry

The AI app and agent factory

Microsoft Foundry is the enterprise AI platform to build, ground, and govern AI apps and agents at scale. It brings together your full agent lifecycle with open development, built-in intelligence, and consistent security, compliance, and policy controls across every agent.

Microsoft Foundry Local

Build local AI into your app

Use native SDKs to download, cache, load, and call optimized local models on-device.

Tenant AI Assistant (RAG Architecture)

The in-app AI assistant enables tenant users to ask natural language questions about their own data: service history, inventory, technician performance, and billing. This is implemented using Retrieval-Augmented Generation (RAG) with a local Ollama LLM.

evaluation of which AI platform to use

10 — DATA ARCHITECTURE

Data Architecture

Caching Strategy

Cache TypeWhat is CachedTTLInvalidation
Tenant ConfigCache Tenant settings, connection string, plan details1 hourExplicit eviction on tenant settings update
Route CacheOptimized route result per technician per dayUntil next optimization runExplicit on route update or ticket change
Inventory SummaryLow-stock counts, total item counts15 minutesExplicit on stock adjustment
User PermissionsDecoded JWT claims and role cacheJWT expiry (15 min)On token refresh
Geocoding ResultsAddress → lat/lng for known addresses30 daysNever (immutable)

Data Retention & Audit

Roundtrip uses SQL Server Temporal Tables for full audit history on key entities. Every change to ServiceTicket, InventoryItem, and Invoice is automatically tracked with a timestamp and can be queried for compliance or dispute resolution purposes.

  • Temporal tables on: ServiceTickets, InventoryItems, Invoices, Clients
  • Soft deletes on all tenant-owned entities — IsDeleted flag, never physical DELETE
  • Hangfire job history retained for 30 days (configurable per tenant plan)
  • Log retention: 90 days in Seq/Application Insights (configurable)

Database Connectivity

// TenantContext resolves connection string at runtime
// Standard tenants use shared connection string (row-level isolation)
// Enterprise tenants get their own connection string from TenantRegistry

public class TenantAwareDbContext : ApplicationDbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
var connString = _tenantContext.Plan == TenantPlan.Enterprise
? _tenantContext.ConnectionString // dedicated DB
: _config["ConnectionStrings:Shared"]; // shared DB

options.UseSqlServer(connString);
}
}

11 — SECURITY ARCHITECTURE

Security Architecture

Authentication Flow

Microsoft Entra External ID

Microsoft Entra External ID

Image is from Microsoft's website and is their property

Authorization Model

RolePermissions Summary
TenantAdminFull access to all tenant data. User management, settings, billing, reports. Cannot access other tenants.
DispatcherCreate/assign/update tickets. View all technicians and routes. View inventory. Generate quotes. Cannot manage users or billing.
TechnicianView own assigned tickets. Update ticket status and part usage. Update own location. View own route. Read-only on other data.
BillingStaffView all tickets (read-only). Full access to invoices, quotes, payment records. Cannot assign tickets or manage users.
ReadOnlyRead-only access to all tenant data. No mutations permitted. Suitable for reporting integrations.

Security Controls

ThreatControl
Cross-tenant data leakageEF Core Global Query Filter (primary) + SQL Server Row-Level Security (defence-in-depth) + NetArchTest enforcement
Unauthorized API accessEntra ID JWT validation on every request. No endpoint is accessible without a valid tenant-scoped token.
Injection attacksFluentValidation on all inputs. EF Core parameterized queries. Dapper parameterized queries. No raw SQL string concatenation.
Broken object-level authAll resource lookups include TenantId in WHERE clause. Result.NotFound returned for cross-tenant ID guessing.
Sensitive data exposureNo PII logged (addresses, phone numbers masked in Serilog). Connection strings in Azure Key Vault / environment variables.
Background job tamperingHangfire dashboard protected by TenantAdmin role. Jobs enqueued only from authenticated API context with tenant claim.
AI prompt injectionTenant AI assistant prompts validated and sanitized. System prompt instructs model to only answer from provided context.

12 — DEPLOYMENT ARCHITECTURE

Deployment Architecture

Containerization

Every Roundtrip component is packaged as a Docker container, enabling consistent environments across development, staging, and production. Docker Compose orchestrates the full stack locally; Kubernetes (or Docker Swarm) handles production orchestration.

# docker-compose.yml — Development Stack
services:
api: # ASP.NET Core 10 API
frontend: # React 19 dev server (Vite)
sqlserver: # SQL Server 2022
redis: # Redis 7 (cache + SignalR backplane)
*n8n: # n8n workflow automation
*ollama: # Ollama LLM runtime
seq: # Seq log viewer (dev only)
hangfire-ui: # Hangfire dashboard (api sidecar)

* Pending review

Production Topology

13 — QUALITY ATTRIBUTES

Quality Attribute Strategies

Quality AttributeTargetStrategy
PerformanceAPI P95 < 200ms for reads, < 500ms for writesRedis caching, Dapper for complex queries, EF Core compiled queries, async throughout, pagination enforced
ScalabilitySupport 500+ concurrent tenants, horizontal API scalingStateless API instances, Redis distributed cache, SignalR Redis backplane, connection pooling
Availability99.9% uptime SLAActive-active API instances, SQL Server read replica, Redis cluster, Keycloak cluster, health checks with auto-restart
SecurityZero cross-tenant data leakage, no PII in logsEF Core Global Query Filter, SQL RLS, JWT validation, FluentValidation on all inputs, Serilog PII masking
MaintainabilityNew feature in 1 bounded context does not break othersClean Architecture enforced by NetArchTest, CQRS keeps handlers small, Domain Events decouple modules
Testability80%+ code coverage, no flaky testsTestcontainers for real DB/Redis in tests, Result pattern (no exception flow), Bogus for test data generation
ObservabilityFull trace from HTTP request to DB queryOpenTelemetry distributed tracing, Serilog structured logging enriched with TenantId, Hangfire dashboard

14 Summary of Changes

This will be the log of changes to this document between versions

v1: Initial Version v1.1: Adding to Ops Manual, adjusting from keycloak to Entra Id and AI functionality review