Skip to content

Inter Service Communication

Sergio Hernandez edited this page Jul 24, 2026 · 1 revision

Inter-Service Communication

Every service-to-service call in TrackHub is GraphQL over HTTPS. There is no message bus, no gRPC and no direct cross-service database access — a service that needs another service's data either calls it, or maps a small read-only projection of its tables (see Database).

Related pages: Architecture · Security and Identity · Testing Strategy


The call graph

graph LR
    sync["SyncWorker"]
    rtr["Router"]
    mgr["Manager"]
    tele["Telemetry"]
    geo["Geofencing"]
    trip["TripManagement"]
    rpt["Reporting"]
    sec["Security"]

    sync --> rtr
    rtr --> mgr
    rtr --> tele
    rtr --> geo
    rtr --> trip
    mgr --> sec
    mgr --> rtr
    sec --> mgr
    geo --> mgr
    trip --> mgr
    trip --> tele
    rpt --> mgr
    rpt --> rtr
    rpt --> tele
    rpt --> geo
    rpt --> trip
Loading
From To Purpose
SyncWorker Router Drives the position, device and health sync loops
Router Manager Operators, devices, credentials, master data, manual-sync dispatch
Router Telemetry Latest-position and history writes, health-check and sync-run records
Router Geofencing processPositions — the geofence detection feed
Router TripManagement processTripPositions — the trip detection feed
Manager Security User creation, identity and authorization checks
Manager Router Manual sync dispatch (120 s timeout — the call awaits a provider fetch)
Security Manager Account and master-data reads
Geofencing Manager Alert emission, background job runs
TripManagement Manager Alerts, job runs, driver/transporter/group validation, document metadata, public-link grants
TripManagement Telemetry positionHistoryRange for route replay
Reporting Manager, Router, Telemetry, Geofencing, TripManagement Report datasets

TripManagement makes no call to Geofencing — it reads geofencing.geofences as a read-only EF projection, once, to snapshot a stop's arrival area when a trip starts.


Client registration

Clients are created through IGraphQLClientFactory.CreateClient(Clients.X[, asService]), where Clients is the shared constant catalog (Hub, Geofence, Identity, Manager, Router, Security, Telemetry, TripManagement). The endpoint comes from configuration key AppSettings:GraphQL{ServiceName}Service.

Registration is centralized. Every named client is registered through services.AddGraphQLClient(name, …) or AddGraphQLServiceClient(name) in Common.Infrastructure — never a raw AddHttpClient. The extension applies:

  • a 30 s timeout (Manager → Router uses 120 s)
  • Authorization and x-correlation-id header propagation
  • an explicit resilience choice
Policy When Services using it
NoRetry Default. Timeout + circuit breaker only. GraphQL is always POST, so a retried mutation is a duplicated mutation. Everything not listed below
WithRetry Query-only clients, where a retry is safe The Identity client; Reporting's Router, Geofence and Telemetry readers

An unregistered client name silently yields a default HttpClient — 100 s timeout, no header propagation, no resilience. Always register through the extension.


Caller identity vs service identity

This is the single most important rule in this area.

Tenant scope follows the CALLER; provider credentials are read with the SERVICE identity.

A service registers both client variants (AddGraphQLClient + AddGraphQLServiceClient) and picks per call:

  • the propagating client for reads whose scope must follow the caller — the caller's bearer token is forwarded, so the producer applies that user's account and group visibility;
  • the service client (asService: true, or AuthorityServer:IsService = true for a whole host) for work the platform performs on the caller's behalf, using the host's own client-credentials identity.

The Router uses this split to keep GPS provider credentials out of user permissions entirely. Every interactive provider-touching surface — live map, per-transporter position, history replay, trips, device listing, ping, manual sync — resolves the operator through IOperatorReader (propagating, so Manager applies the caller's visibility), then reads the decrypted credential through IOperatorSystemReader (service identity). A user's permissions are never widened to obtain credential material.

Two permissions are deliberately distinct:

Grant Means Held by
Operators/Custom "may operate this GPS integration" — ping, manual sync Manager and User roles
Credentials/Custom "may view decrypted credential material" Credential administration only — granting it anywhere also grants sight of provider secrets

Naming convention

The *SystemReader / *SystemWriter suffix marks the client-credentials counterpart of a propagating client: PositionSystemWriter, PositionHistorySystemWriter, OperatorHealthCheckSystemWriter, ResolvedAddressSystemWriter, OperatorSystemReader, GeocodingProviderReader.

The SyncWorker registers everything with headerPropagation: false and IsService: true, so both variants resolve to its own identity there — it has no user to propagate.


Service-side setup for a service identity

Every service-identity call needs two things on the Security side, or it is denied:

  1. the client name registered in security.clients;
  2. a matching row in security.service_client_permissions — client, resource, action, scope and audience.

Both are seeded by the Security DBInitializer. A missing registration or grant produces a clean FORBIDDEN naming the required Resource.Action.

Current platform-internal grants include:

Identity Grants
router_client / syncworker_client The Telemetry surface, the Manager master-data surface, Geofencing/Custom (without which the SyncWorker's processPositions feed is denied), and TripTracking/Customand nothing else in the trip module, so those two identities can call processTripPositions only
security_client Audit/Write
geofence_client Alerts/Write, BackgroundJobs/Write
trip_client Alerts/Write, BackgroundJobs/Write, Drivers/Read, Transporters/Read, Groups/Read, Documents/Read, `PublicLinks/Write

Query documents are single-source-of-truth constants

Each client class holds its GraphQL documents as internal const string fields, exposed to the contract test suite via [assembly: InternalsVisibleTo("TrackHub.ServiceContracts.Tests")].

Never re-type a query string in a test. Reference the production constant, so a drifted document fails the test instead of silently passing against a stale copy. See Testing Strategy.

Enum values travel in GraphQL variables, invisible to document validation. Every enum-typed literal a client sends (checkType, triggerType, result, …) needs a round-trip (Layer B) test asserting it coerces into the producer's enum. Document validation alone will not catch a renamed enum member.


Caching and token handling

  • Authorization decisions are cached for 30 s in Common.Infrastructure.IdentityService — both the user decision (authorizeUser) and the service decision (isValidServiceForResource). Configurable via AppSettings:AuthorizationCacheSeconds; 0 disables it. This is the same staleness window as account status and feature flags.
  • The client-credentials token is cached process-wide, with a 60 s expiry margin.
  • Provider sessions and tokens are cached across sync cycles — see Router.

Batched cross-service reads

Several hot paths are batched deliberately, to avoid an N+1 across the network:

Call Consumer Why
Manager allAccountFeaturesMaster SyncWorker, feature-matrix report One read instead of one per account
Manager transporterDocumentCompliance Documents report
Manager operatorsMaster SyncWorker Carries credentials for service principals, so the worker never re-fetches operators one by one
Telemetry transporterPositionsByOperators Live-map hot path

Keyless service-called surfaces are invisible to the account-scope coverage gate. The gate (Common.Application.Testing.AccountScopeCoverage) only sees wire keys, so a batched …Master query is caught only by enumerating the consumers' internal const documents and each client's identity mode. Do that enumeration for every new cross-service call.


Wire conventions

  • Attribute naming is hourmeter everywhere. The CommandTrack provider payload calls it hobbsMeter and is remapped only at that provider boundary, with [JsonPropertyName("hobbsMeter")].
  • AttributesVm.Hourmeter is expressed in hours. Mileage has no conversion applied at the mapper boundary, so a consumer must not assume metres or kilometres without checking the provider.
  • Timestamps are DateTimeOffset in UTC, end to end. See Database.
  • Delete mutations return the deleted entity's identifier (typed to follow the entity key — Guid, long or int), not a boolean.
  • x-correlation-id propagates across every hop, so one user action can be traced through the whole call graph in the centralized logs.

Clone this wiki locally