Skip to content

Service Communication

This page documents how Microtec's 13 backend microservices communicate with each other, with the API Gateway, and with external systems. Three distinct communication tiers exist depending on whether the source and destination are in the same Container Apps Environment or crossing the VNet boundary.


Communication Tiers


Tier 1: Intra-CAE Communication (HTTP)

Services within the same Container Apps Environment communicate over plain HTTP using Azure Container Apps' built-in internal DNS.

http://{app-name}

For example, AppsPortal calling NotificationService:

csharp
// base URL resolves to http://notification-service (internal DNS)
var response = await _httpClient.PostAsJsonAsync(
    "api/v1/notifications/send",
    new SendNotificationRequest { ... });

mTLS in Private CAE

Although the protocol is HTTP from the application's perspective, the Container Apps Environment handles mTLS transparently. All traffic between services in the private CAE is encrypted and mutually authenticated at the sidecar level. Application code does not need to manage certificates.


Tier 2: Cross-CAE Communication (HTTPS)

Communication between the public CAE (Gateway/Keycloak) and the private CAE uses HTTPS. The private CAE exposes services only on their internal FQDN within the VNet:

https://{app-name}.internal.{cae-domain}

The Gateway's Ocelot/YARP configuration stores these upstream URLs per environment. No service in the private CAE is directly accessible from the internet.


Tier 3: External Communication (FQDN)

External calls — to ZATCA, ETA, or other third-party APIs — always go through AFD egress or directly via the Container App's outbound IP. These calls use HTTPS with standard certificate validation.


Service Communication Matrix

ServiceCAEConsumesConsumed By
GatewayPublicAll private CAE services (upstream routing)AFD
KeycloakPublicAll services (JWT validation)
AppsPortalPrivateNotification, Attachment, Workflow, ReportingGateway
BusinessOwnersPrivateNotification, AttachmentGateway
HR ServicePrivateNotification, AttachmentGateway
InventoryPrivateAttachment, ReportingGateway, AppsPortal
WorkflowPrivateNotificationGateway, all services (via ASB)
NotificationPrivateTemplateGateway, all services
AttachmentPrivateGateway, all services
Integration / ZATCAPrivateNotificationGateway, Worker (via ASB)
ReportingPrivateGateway, Worker
ImportPrivateAttachmentGateway
Platforms WorkerPrivateAll services (via ASB)— (background job runner)
TemplatePrivateNotification

Inter-Service HTTP Clients (Microtec.PublicApi.*)

Rather than hand-coding HttpClient calls, each service that exposes a public API surface to other services publishes a typed HTTP client as a NuGet package.

PackageClient InterfaceUsed By
Microtec.PublicApi.AppsPortalIAccountingPublicApiHR, Inventory, Worker
Microtec.PublicApi.BusinessOwnersIBusinessOwnersPublicApiAppsPortal, Worker
Microtec.PublicApi.NotificationINotificationPublicApiAll services
Microtec.PublicApi.AttachmentIAttachmentPublicApiAll services
Microtec.PublicApi.WorkflowIWorkflowPublicApiAppsPortal, HR, Inventory

Usage Example

csharp
// Registration (in consuming service's Program.cs)
builder.Services.AddNotificationPublicApi(
    baseUrl: builder.Configuration["Services:NotificationBaseUrl"]!);

// Injection and use
public class SendInvoiceEmailCommandHandler
    : ICommandHandler<SendInvoiceEmailCommand, Unit>
{
    private readonly INotificationPublicApi _notifications;

    public async Task<Unit> Handle(
        SendInvoiceEmailCommand request,
        CancellationToken cancellationToken)
    {
        await _notifications.SendEmailAsync(new EmailRequest
        {
            To = request.RecipientEmail,
            Subject = $"Invoice {request.InvoiceNumber}",
            TemplateId = NotificationTemplates.InvoiceCreated,
            Parameters = new { request.InvoiceNumber, request.Amount }
        }, cancellationToken);

        return Unit.Value;
    }
}

Polly Resilience

All Microtec.PublicApi.* clients include pre-configured Polly policies: 3 retries with exponential backoff (200ms, 400ms, 800ms) and a circuit breaker that opens after 5 consecutive failures. These defaults are applied automatically when using the AddXxxPublicApi() extension methods.


XApiKey — Internal Service Authentication

Some direct service-to-service calls bypass user JWT authentication entirely. These use a shared static API key stored in Key Vault and injected as an environment variable.

XApiKey Security Notice

The XApiKey is for trusted internal integrations only. It must never be exposed in client-side code, logs, or documentation. The actual key value is stored in Azure Key Vault (XApiKey secret) and is referenced via keyvaultref: in the Container App environment. Do not hardcode it anywhere.

Usage pattern:

csharp
// In the caller (adds header automatically via DelegatingHandler)
builder.Services.AddHttpClient<IWorkflowPublicApi, WorkflowPublicApiClient>()
    .AddXApiKeyHandler();  // injects X-Api-Key header from ITenantProvider or config

// In the receiver (validation middleware)
app.UseMiddleware<XApiKeyMiddleware>();
// Checks request.Headers["X-Api-Key"] against configured value

Gateway Routing

The Gateway (Gateway/) uses Ocelot for static route configuration and YARP for dynamic or performance-critical routes. The upstream URL for each downstream service is injected per environment via appsettings.{environment}.json or environment variables.

json
// ocelot.json excerpt
{
  "Routes": [
    {
      "DownstreamPathTemplate": "/api/v1/invoices/{everything}",
      "DownstreamScheme": "http",
      "DownstreamHostAndPorts": [{ "Host": "appsportal-api", "Port": 80 }],
      "UpstreamPathTemplate": "/api/v1/invoices/{everything}",
      "UpstreamHttpMethod": ["GET", "POST", "PUT", "DELETE"],
      "AuthenticationOptions": {
        "AuthenticationProviderKey": "Bearer"
      }
    }
  ]
}

JWT Validation at Gateway

The Gateway validates all JWTs against Keycloak's JWKS endpoint before forwarding requests downstream. Backend services trust the Gateway and do not re-validate JWTs (except for the XApiKey path, which uses its own middleware).


Service Discovery

Microtec does not use a dedicated service registry (no Consul, no Eureka). Service discovery relies on Azure Container Apps' built-in DNS:

  • Within the same CAE: http://{container-app-name} resolves to the service's internal IP.
  • The container app name is defined in services-config.json and matches the Bicep resource name.
  • The Gateway's upstream URLs are set at deployment time via environment variables.

Health Checks

Every service exposes:

EndpointPurpose
GET /healthCombined readiness + liveness probe
GET /health/readyReadiness only (DB connection, Redis reachable)
GET /health/liveLiveness only (process alive)

Container Apps are configured to probe /health/ready before marking a replica as in-service. Failed probes trigger a restart.

csharp
// In Program.cs (all services via Microtec.Web.Hosting package)
builder.Services
    .AddHealthChecks()
    .AddSqlServer(connectionString, name: "sql-tenant")
    .AddRedis(redisConfig, name: "redis")
    .AddAzureServiceBusTopic(asbConnection, topicName, name: "asb");

app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
    Predicate = check => check.Tags.Contains("ready")
});
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false  // always healthy if process is running
});

Internal Documentation — Microtec Platform Team