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
| Goal | Description | Priority |
|---|---|---|
| Multi-Tenancy | Complete data isolation between tenants at every layer — database, application, and identity | Critical |
| Scalability | Horizontal scaling of API servers without architectural changes; tenant-specific database scaling | High |
| Reliability | Horizontal scaling of API servers without architectural changes; tenant-specific database scaling | High |
| Data Sovereignty | AI inference runs locally via Ollama — tenant data never leaves the infrastructure | High |
| Developer Productivity | Clean Architecture, CQRS, and DDD patterns enable rapid feature development without regressions | Medium |
| Observability | Every request, job, and AI call is traceable via OpenTelemetry with tenant context | Medium |
| Extensibility | API-first design allows third-party integrations (accounting, CRM) without core changes | Medium |
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
| Layer | Project Name | Responsibilities | Key Technologies |
|---|---|---|---|
| Domain | Roundtrip.Core | Entities, Value Objects, Domain Events, Aggregate Roots, Domain Interfaces | Pure C# — no external dependencies |
| Application | Roundtrip.UseCases | CQRS Handlers, Use Cases, Application Services, Validation Pipeline, DTOs | Mediator, FluentValidation, Ardalis.Specification |
| Infrastructure | Roundtrip.Infrastructure | EF Core DbContext, Repositories, External API Clients, Background Jobs, Email/SMS | EF Core, Dapper, Redis, Hangfire, SendGrid, Twilio |
| API | Roundtrip.Api | FastEndpoints, SignalR Hubs, Authentication Middleware, Tenant Resolution, OpenAPI | ASP.NET Core 9, FastEndpoints, Entra ID, JWT, SignalR |
| Tests | Roundtrip.Tests.* | Unit tests (domain/application), Integration tests | xUnit, 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
| Behavior | Applies To | Responsibility |
|---|---|---|
| LoggingBehavior | All requests | Logs request type, tenant ID, duration, and outcome to Serilog with structured fields |
| ValidationBehavior | All requests | Executes FluentValidation validators; returns 400 with problem details on failure without reaching handler |
| TenantScopingBehavior | All requests | Resolves TenantContext from JWT claim and injects into scoped services; prevents handler executing without valid tenant |
| CachingBehavior | Queries only | Checks Redis cache for query result; on miss, executes handler and populates cache with tenant-scoped key |
| TransactionBehavior | Commands only | Wraps 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.
| Tier | Isolation Model | Database Strategy | Target Customer |
|---|---|---|---|
| Basic | Shared Schema | Single DB, TenantId column on every table, EF Core Global Query Filter | Small businesses (1–3 technicians) |
| Standard | Shared Schema | Single DB, TenantId column on every table, EF Core Global Query Filter | Small businesses (1–10 technicians) |
| Professional | Schema Isolation | Single 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 Root | Bounded Context | Key Entities / Value Objects | Domain Events |
|---|---|---|---|
| ServiceTicket | Tickets | TicketId, ClientId, TechnicianId, ServiceAddress (VO), TicketStatus (Enum), PartUsages, Notes | TicketCreated, TicketAssigned, TicketCompleted, TicketCancelled |
| Client | Clients | ClientId, ContactInfo (VO), ServiceAddress (VO), ServiceHistory | ClientCreated, ClientUpdated |
| Technician | Users | TechnicianId, UserId, Skills, CurrentLocation (VO), Schedule | TechnicianAssigned, LocationUpdated |
| InventoryItem | Inventory | ItemId, SKU, PartName, Quantity, ReorderThreshold, Location | StockLow, ItemConsumed, ItemRestocked |
| Invoice | Billing | InvoiceId, TicketId, LineItems, TotalAmount, InvoiceStatus | InvoiceGenerated, InvoiceSent, PaymentReceived |
| TechnicianRoute | RouteId, TechnicianId, Date, Stops (VO list), OptimizedOrder | RouteOptimized, RouteUpdated | |
| Tenant | Identity | TenantId, TenantSlug, Plan, Settings, ConnectionString (Enterprise) | TenantProvisioned, PlanChanged |
Key Value Objects
| Value Object | Properties | Invariants |
|---|---|---|
| ServiceAddress | Street, City, State, PostalCode, Country, Latitude, Longitude | PostalCode must match City/State. Lat/Lng populated on geocoding. Cannot be partially constructed. |
| ContactInfo | FirstName, LastName, Email, PrimaryPhone, SecondaryPhone | Email must be valid format. PrimaryPhone required. At least one contact method required. |
| Money | Amount (decimal), Currency (ISO 4217) | Amount cannot be negative. Currency must be valid ISO code. Arithmetic preserves currency. |
| TicketStatus | Enum: Open, Assigned, InProgress, OnHold, Completed, Cancelled | Status transitions follow defined state machine. Completed/Cancelled are terminal states. |
| GPS Coordinate | Latitude, Longitude | Lat: -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).
| Convention | Rule |
|---|---|
| Base URL | https://api.roundtrip.app/v1/ (or https://{tenant}.roundtrip.app/api/v1/) |
| Versioning | URL path versioning: /v1/, /v2/ — new major version on breaking changes only |
| Authentication | Bearer JWT (Entra ID access token) in Authorization header |
| Tenant Scoping | Resolved from JWT tid claim; all resources automatically scoped to tenant |
| Response Format | application/json — all responses. Problem Details (RFC 7807) for errors |
| Pagination | Cursor-based pagination for list endpoints: ?cursor=&limit=50 |
| Filtering | Query string parameters: ?status=open&technicianId=&from=&to= |
| HTTP Methods | GET (read), POST (create), PUT (full update), PATCH (partial update), DELETE |
| Status Codes | 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401, 403, 404, 409 Conflict, 422 Unprocessable |
Key API Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /v1/tickets | List tickets for tenant (paginated, filterable) | Dispatcher+ |
| POST | /v1/tickets | Create new service ticket | Dispatcher+ |
| GET | /v1/tickets/{id} | Get ticket by ID with full details | All roles |
| PATCH | /v1/tickets/{id}/status | Update ticket status (state machine enforced) | Technician+ |
| POST | /v1/tickets/{id}/parts | Record part usage on ticket (photo + quantity) | Technician |
| GET | /v1/clients | List clients with service history summary | Dispatcher+ |
| POST | /v1/clients | Create new client record | Dispatcher+ |
| GET | /v1/clients/{id}/history | Full service history for a client | Dispatcher+ |
| GET | /v1/routing/{technicianId}/today | Get optimized route for technician today | Technician, Dispatcher |
| POST | /v1/routing/optimize` | Trigger route optimization (calls n8n/Ollama) | Dispatcher |
| GET | /v1/inventory | List inventory items with stock levels | All roles |
| PATCH | /v1/inventory/{id}/quantity | Adjust stock quantity | TenantAdmin, Dispatcher |
| GET | /v1/inventory/low-stock | Items below reorder threshold | Dispatcher+ |
| POST | /v1/invoices | Generate invoice from completed ticket | BillingStaff+ |
| POST | /v1/invoices/{id}/send | Send invoice via email/SMS | BillingStaff+ |
| GET | /v1/invoices/{id}/pdf | Download invoice as PDF | BillingStaff+ |
| GET | /v1/technicians | List technicians with current status | Dispatcher+ |
| PATCH | /v1/technicians/{id}/location | Update technician GPS location | Technician (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.



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.
| Job | Type | Trigger | Retry Policy |
|---|---|---|---|
| SendInvoiceEmail | Fire-and-forget | Invoice.Send command via IMediator | 3 retries, 5min exponential backoff |
| SendInvoiceSMS | Fire-and-forget | Invoice.Send command via IMediator | 3 retries, 5min exponential backoff |
| GenerateInvoicePdf | Fire-and-forget | Ticket completed domain event | 3 retries, immediate |
| OptimizeRoutes | Recurring | Daily at 5:00 AM per tenant timezone | 2 retries, alert on final failure |
| LowStockAlert | Recurring | Every 6 hours per tenant | 2 retries |
| TenantDailyReport | Recurring | Daily at 8:00 AM per tenant timezone | 2 retries |
| PurgeExpiredCache | Recurring | Daily at 2:00 AM (system) | 1 retry |
| TenantOnboarding | Continuation chain | Tenant registration: provision DB → create realm → seed data → send welcome | Per-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:
- n8n -- Ollama self contained AI platform
- Microsoft Foundry - Azure Service
- 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 Type | What is Cached | TTL | Invalidation |
|---|---|---|---|
| Tenant Config | Cache Tenant settings, connection string, plan details | 1 hour | Explicit eviction on tenant settings update |
| Route Cache | Optimized route result per technician per day | Until next optimization run | Explicit on route update or ticket change |
| Inventory Summary | Low-stock counts, total item counts | 15 minutes | Explicit on stock adjustment |
| User Permissions | Decoded JWT claims and role cache | JWT expiry (15 min) | On token refresh |
| Geocoding Results | Address → lat/lng for known addresses | 30 days | Never (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

Image is from Microsoft's website and is their property
Authorization Model
| Role | Permissions Summary |
|---|---|
| TenantAdmin | Full access to all tenant data. User management, settings, billing, reports. Cannot access other tenants. |
| Dispatcher | Create/assign/update tickets. View all technicians and routes. View inventory. Generate quotes. Cannot manage users or billing. |
| Technician | View own assigned tickets. Update ticket status and part usage. Update own location. View own route. Read-only on other data. |
| BillingStaff | View all tickets (read-only). Full access to invoices, quotes, payment records. Cannot assign tickets or manage users. |
| ReadOnly | Read-only access to all tenant data. No mutations permitted. Suitable for reporting integrations. |
Security Controls
| Threat | Control |
|---|---|
| Cross-tenant data leakage | EF Core Global Query Filter (primary) + SQL Server Row-Level Security (defence-in-depth) + NetArchTest enforcement |
| Unauthorized API access | Entra ID JWT validation on every request. No endpoint is accessible without a valid tenant-scoped token. |
| Injection attacks | FluentValidation on all inputs. EF Core parameterized queries. Dapper parameterized queries. No raw SQL string concatenation. |
| Broken object-level auth | All resource lookups include TenantId in WHERE clause. Result.NotFound returned for cross-tenant ID guessing. |
| Sensitive data exposure | No PII logged (addresses, phone numbers masked in Serilog). Connection strings in Azure Key Vault / environment variables. |
| Background job tampering | Hangfire dashboard protected by TenantAdmin role. Jobs enqueued only from authenticated API context with tenant claim. |
| AI prompt injection | Tenant 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 Attribute | Target | Strategy |
|---|---|---|
| Performance | API P95 < 200ms for reads, < 500ms for writes | Redis caching, Dapper for complex queries, EF Core compiled queries, async throughout, pagination enforced |
| Scalability | Support 500+ concurrent tenants, horizontal API scaling | Stateless API instances, Redis distributed cache, SignalR Redis backplane, connection pooling |
| Availability | 99.9% uptime SLA | Active-active API instances, SQL Server read replica, Redis cluster, Keycloak cluster, health checks with auto-restart |
| Security | Zero cross-tenant data leakage, no PII in logs | EF Core Global Query Filter, SQL RLS, JWT validation, FluentValidation on all inputs, Serilog PII masking |
| Maintainability | New feature in 1 bounded context does not break others | Clean Architecture enforced by NetArchTest, CQRS keeps handlers small, Domain Events decouple modules |
| Testability | 80%+ code coverage, no flaky tests | Testcontainers for real DB/Redis in tests, Result pattern (no exception flow), Bogus for test data generation |
| Observability | Full trace from HTTP request to DB query | OpenTelemetry 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