Skip to content

Inter-Service Communication

Microtec microservices communicate with each other using typed HTTP clients generated from the Microtec.PublicApi.* NuGet packages. All inter-service calls bypass Keycloak JWT authentication and use the shared XApiKey header for service identity.


Communication Model

All inter-service traffic routes through the Ocelot API Gateway. Services do not call each other's private CAE FQDNs directly. This ensures:

  • A single choke point for circuit breaking and retry policies
  • Uniform observability (all calls appear in Gateway logs)
  • Consistent XApiKey validation

PublicApi NuGet Packages

Each service that exposes a public API consumed by other services provides a corresponding NuGet package:

PackagePublished byConsumed by
Microtec.PublicApi.AppsPortal.AccountingAccounting serviceHR, Sales, Inventory
Microtec.PublicApi.AppsPortal.InventoryInventory serviceSales, Distribution
Microtec.PublicApi.AppsPortal.HRHR servicePayroll, Reporting
Microtec.PublicApi.NotificationNotification serviceAll services
Microtec.PublicApi.AttachmentAttachment serviceAll services
Microtec.PublicApi.WorkflowWorkflow serviceAccounting, HR

Packages are published to the Microtec Azure DevOps NuGet feed and versioned with the service release.


XApiKey Header

The XApiKey header identifies the caller as a trusted internal service:

XApiKey: 3bb564df-0f24-4ea6-82c1-d99f368cac8a
PropertyValue
Header nameXApiKey
Value sourceKey Vault secret XApiKey (per environment)
Gateway validationValidates on routes tagged internal in Ocelot config
Client presentationAdded automatically by all Microtec.PublicApi.* clients

XApiKey Differs Per Environment

The XApiKey value is environment-specific (different in dev, stage, preprod, uat, production). Services read it from their configuration at startup. When calling between environments (e.g. a local dev service calling stage), ensure the correct key is configured.


Typed HTTP Client Registration

Services register PublicApi clients using the shared extension:

csharp
// In Accounting service Program.cs
builder.Services.AddMicrotecPublicApiClients(builder.Configuration);

// Internally in Microtec.Web.Core
public static IServiceCollection AddMicrotecPublicApiClients(
    this IServiceCollection services,
    IConfiguration configuration)
{
    var gatewayBaseUrl = configuration["Gateway:BaseUrl"]!;
    var xApiKey = configuration["XApiKey"]!;

    services.AddHttpClient<IInventoryPublicApi, InventoryPublicApiClient>(client =>
    {
        client.BaseAddress = new Uri(gatewayBaseUrl);
        client.DefaultRequestHeaders.Add("XApiKey", xApiKey);
        client.Timeout = TimeSpan.FromSeconds(30);
    })
    .AddResilienceHandler("inventory-resilience", ConfigureResilience);

    return services;
}

Polly Resilience Policies

All inter-service HTTP clients are configured with Polly resilience policies via Microsoft.Extensions.Http.Resilience:

csharp
private static void ConfigureResilience(ResiliencePipelineBuilder<HttpResponseMessage> builder)
{
    builder
        // Retry: 3 attempts with exponential backoff (1s, 2s, 4s)
        .AddRetry(new HttpRetryStrategyOptions
        {
            MaxRetryAttempts = 3,
            Delay = TimeSpan.FromSeconds(1),
            BackoffType = DelayBackoffType.Exponential,
            UseJitter = true,
            ShouldHandle = new PredicateBuilder<HttpResponseMessage>()
                .Handle<HttpRequestException>()
                .HandleResult(r => r.StatusCode is
                    HttpStatusCode.RequestTimeout or
                    HttpStatusCode.TooManyRequests or
                    HttpStatusCode.BadGateway or
                    HttpStatusCode.ServiceUnavailable or
                    HttpStatusCode.GatewayTimeout)
        })
        // Circuit breaker: open after 5 failures in 30s, half-open after 60s
        .AddCircuitBreaker(new HttpCircuitBreakerStrategyOptions
        {
            FailureRatio = 0.5,
            SamplingDuration = TimeSpan.FromSeconds(30),
            MinimumThroughput = 5,
            BreakDuration = TimeSpan.FromSeconds(60)
        })
        // Timeout per attempt
        .AddTimeout(TimeSpan.FromSeconds(10));
}

Resilience Policy Summary

PolicyConfigBehaviour
Retry3 attempts, exponential backoffRetries on network errors and 5xx/429
Circuit breaker50% failure rate in 30s, 5 min breakOpens circuit after repeated failures
Timeout10s per attemptFails fast instead of blocking indefinitely

Tenant Context Propagation

When one service calls another on behalf of a user, the tenant context must be propagated:

csharp
// Accounting service calling Inventory service
public class GetItemDetailsHandler
{
    private readonly IInventoryPublicApi _inventoryApi;
    private readonly ITenantProvider _tenantProvider;

    public async Task<ItemDetails> Handle(GetItemDetailsQuery query, CancellationToken ct)
    {
        // TenantId is automatically added as a header by the client delegating handler
        return await _inventoryApi.GetItemById(query.ItemId, ct);
    }
}

A TenantContextDelegatingHandler in the Microtec.PublicApi.* client adds the X-Tenant-Id header automatically:

csharp
public class TenantContextDelegatingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken ct)
    {
        request.Headers.Add("X-Tenant-Id", _tenantProvider.TenantId);
        return await base.SendAsync(request, ct);
    }
}

The receiving service reads X-Tenant-Id from the request headers and uses it to scope the database query — it does not trust the caller's tenant claim blindly. The XApiKey header is the trust signal; X-Tenant-Id is the data signal.


Error Handling

Inter-service call errors are handled by the calling service:

csharp
try
{
    var item = await _inventoryApi.GetItemById(query.ItemId, ct);
    return item;
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    throw new NotFoundException($"Inventory item {query.ItemId} not found");
}
catch (BrokenCircuitException)
{
    throw new ServiceUnavailableException("Inventory service is temporarily unavailable");
}

Do Not Propagate Internal Errors to Clients

When an inter-service call fails, the calling service returns a user-friendly error (e.g. 503 Service Unavailable with a generic message). Never propagate internal stack traces or service-specific error codes to the ERP API response — this leaks implementation details.

Internal Documentation — Microtec Platform Team