Streamlining Microservice Communication: How Refit HTTP Clients Power Our Distributed Architecture

Streamlining Microservice Communication: How Refit HTTP Clients Power Our Distributed Architecture

In the world of modern software development, microservices architecture has become the gold standard for building scalable, maintainable applications. However, with this architectural approach comes the challenge of seamless inter-service communication. Today, I want to share how we've leveraged Refit, a powerful REST library for .NET, to create a robust and elegant HTTP client infrastructure that forms the backbone of our clients platform.

What is Refit and Why We Chose It

Refit is a REST library for .NET that transforms REST APIs into live interfaces. Instead of writing boilerplate HTTP client code, Refit generates the implementation for you at runtime using attributes to define the HTTP operations. This approach dramatically reduces code complexity while providing type safety and excellent IntelliSense support.

Our platform consists of multiple specialized services: ProfileService, UserService, AdminService, BuddyService, CheckedOutService, and several others. Each service needs to communicate with others to fulfill complex business requirements. Traditional HttpClient usage would result in hundreds of lines of repetitive code, but Refit transforms this into clean, declarative interfaces.

The Refit HTTP Client Portfolio

In our solution, we've implemented a comprehensive suite of HTTP client interfaces, each designed for specific inter-service communication patterns. Let's explore the key clients that power our ecosystem:

Core Service Clients

IProfileHttpClient serves as the central communication hub for profile-related operations. This client handles everything from profile creation and updates to user status management:

[Headers("Content-Type: application/json", "Another-Header: AnotherValue")]
public interface IProfileHttpClient
{
    [Post("/api/profile/v1/integration/profile/upsert/{userName}/{email}/{site}")]
    Task<Result<ProfileServiceModel>> UpsertDefaultProfile(string userName, string email, int site);

    [Post("/api/profile/v1/integration/profile/markloggedoff/{userGuid}/{site}")]
    Task<Result<NoResponseServiceModel>> MarkUserAsLoggedOff(Guid userGuid, int site);

    [Get("/api/profile/v1/integration/honeypot/{userGuid}/{site}")]
    Task<Result<HoneyPotServiceModel>> GetHoneypot(Guid userGuid, int site);
}

Notice how clean and readable this interface is. Each method clearly defines its HTTP verb, route template, and expected response type. The route parameters are automatically bound from method parameters, making the client both intuitive to use and resistant to errors.

ITokenHttpClient handles authentication token management across services:

public interface ITokenHttpClient
{
    [Post("/api/token/v1/token/")]
    Task<Result<TokenServiceModel>> FetchUserServiceToken([FromBody] TokenViewModel model);
}

This client is crucial for our authentication flow, providing service-to-service token exchange capabilities that maintain security boundaries while enabling necessary inter-service communication.

Specialized Domain Clients

IBuddyHttpClient manages relationship and social interaction features:

public interface IBuddyHttpClient
{
    [Get("/api/buddy/v1/integration/{userGuid}/{site}")]
    Task<Result<BuddyListServiceModel>> GetBuddies(Guid userGuid, int site);

    [Get("/api/buddy/v1/relationships/{userGuid}/{site}")]
    Task<Result<BuddyListServiceModel>> GetRelationships(Guid userGuid, int site);

    [Get("/api/buddy/v1/endorsements/{userGuid}/{site}")]
    Task<Result<EndorsementListServiceModel>> GetEndorsements(Guid userGuid, int site);
}

ICheckedOutHttpClient handles profile viewing analytics:

public interface ICheckedOutHttpClient
{
    [Post("/api/checkedout/v1/integration/{userGuid}/{site}")]
    Task<Result<ProfileViewCountServiceModel>> ProfileViewCountAsync(Guid userGuid, int site);

    [Post("/api/checkedout/v1/integration/{site}")]
    Task<Result<GuidListServiceModel>> GetMostPopularProfileViews(int site);
}

These specialized clients demonstrate how Refit enables us to create focused, single-responsibility interfaces that align perfectly with our domain boundaries.

Architectural Integration and Service Registration

The magic of our Refit implementation lies not just in the interface definitions, but in how we integrate them into our dependency injection container. Each service registers its required HTTP clients during startup through dedicated AddHttpClients methods:

void AddHttpClients(WebApplicationBuilder builder)
{
    builder.Services.AddRefitClient<ISocialAdHttpClient>()
           .ConfigureHttpClient(c => c.BaseAddress = new Uri(builder.Environment.EnvironmentName != "LocalDevelopment" ? builder.Configuration["socialadurl"] : "https+http://socialadservice"))
           .AddHttpMessageHandler(() => new AuthHeaderHandler(builder.Services.BuildServiceProvider()));

    builder.Services.AddRefitClient<IImagesHttpClient>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri(builder.Environment.EnvironmentName != "LocalDevelopment" ? builder.Configuration["imagesurl"] : "https+http://imagesservice"))
        .AddHttpMessageHandler(() => new AuthHeaderHandler(builder.Services.BuildServiceProvider()));
}

This configuration pattern provides several key benefits:

  1. Environment-Aware Routing: Our services automatically switch between external URLs for production and local service discovery URLs for development
  2. Automatic Authentication: Most clients include our custom AuthHeaderHandler for seamless token management
  3. Centralized Configuration: All HTTP client settings are managed in one location per service

Authentication and Security Through HTTP Message Handlers

One of the most powerful features of our Refit implementation is the integration with custom HTTP message handlers. Our AuthHeaderHandler demonstrates how cross-cutting concerns like authentication can be elegantly handled:

public class AuthHeaderHandler : DelegatingHandler
{
    private readonly IServiceProvider _serviceProvider;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var userHttpClient = _serviceProvider.GetRequiredService<ITokenHttpClient>();
        var configuration = _serviceProvider.GetRequiredService<IConfiguration>();

        var tokenViewModel = new TokenViewModel
        {
            Token = configuration["UserServiceToken"]
        };
        var tokenResult = await userHttpClient.FetchUserServiceToken(tokenViewModel);
        var token = tokenResult?.Data?.Token;

        if (!string.IsNullOrEmpty(token))
        {
            request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
        }

        return await base.SendAsync(request, cancellationToken);
    }
}

This handler automatically:

  • Fetches authentication tokens when needed
  • Adds Bearer token headers to outgoing requests
  • Handles token refresh scenarios
  • Provides a consistent authentication experience across all services

Multi-Environment Configuration Strategy

Our Refit configuration showcases a sophisticated approach to environment management. Notice how we handle different environments:

builder.Environment.EnvironmentName != "LocalDevelopment" ? 
    builder.Configuration["profileurl"] : 
    "https+http://profileservice"

In local development, we use Aspire's service discovery with the https+http:// scheme, which automatically resolves to the correct local service endpoints. In production and staging environments, we use configuration-driven URLs that point to the appropriate service instances.

This approach provides several advantages:

  • Seamless local development experience
  • Easy deployment across multiple environments
  • No code changes required between environments
  • Built-in support for service discovery and load balancing

Error Handling and Result Patterns

All our Refit clients return a consistent Result<T> pattern that encapsulates both success and failure scenarios:

public class Result<T> where T : ServiceBaseModel 
{
    public bool IsSuccess { get; }
    public bool IsFailure { get; }
    public List<string> Messages { get; set; }
    public Error Error { get; }
    public T? Data { get; set; }
}

This pattern ensures that:

  • All API responses follow a consistent structure
  • Error handling is standardized across services
  • Consumers can easily distinguish between success and failure cases
  • Rich error information is available for debugging and user feedback

Performance and Scalability Considerations

Our Refit implementation includes several performance optimizations:

  1. HTTP Client Factory Integration: All Refit clients are registered using the AddRefitClient extension, which leverages the built-in HTTP client factory for connection pooling and lifecycle management
  2. Async/Await Throughout: Every client method returns Task<T>, ensuring non-blocking I/O operations
  3. Efficient Serialization: Refit uses System.Text.Json by default, providing excellent performance for our JSON-based APIs
  4. Connection Reuse: The underlying HttpClientFactory ensures proper connection pooling and DNS refresh cycles

Integration with .NET Aspire

Our solution leverages .NET Aspire for cloud-native development, and Refit integrates seamlessly with this ecosystem. The builder.AddServiceDefaults() call in each service includes:

  • Service Discovery: Automatic resolution of service endpoints
  • Resilience Patterns: Built-in retry policies and circuit breakers
  • Observability: Automatic OpenTelemetry instrumentation for HTTP calls
  • Health Checks: Integrated health monitoring for all HTTP dependencies

Real-World Usage Patterns

Let's examine how these clients are used in practice. In our ProfileService, we inject multiple HTTP clients to gather comprehensive profile information:

public class ProfileBusiness : IProfileBusiness
{
    private readonly IBuddyHttpClient _buddyHttpClient;
    private readonly ICheckedOutHttpClient _checkedOutHttpClient;
    private readonly IImagesHttpClient _imagesHttpClient;

    public async Task<ProfileViewModel> GetCompleteProfile(Guid userGuid, Site site)
    {
        var profileTask = GetBaseProfile(userGuid, site);
        var buddiesTask = _buddyHttpClient.GetBuddies(userGuid, (int)site);
        var viewsTask = _checkedOutHttpClient.ProfileViewCountAsync(userGuid, (int)site);
        var imagesTask = _imagesHttpClient.GetImages(userGuid, (int)site);

        await Task.WhenAll(profileTask, buddiesTask, viewsTask, imagesTask);

        return BuildCompleteProfile(
            profileTask.Result,
            buddiesTask.Result,
            viewsTask.Result,
            imagesTask.Result
        );
    }
}

This pattern allows us to make parallel HTTP calls to multiple services, dramatically improving response times while maintaining clean, readable code.

Benefits and Lessons Learned

After implementing Refit across our entire microservices ecosystem, we've observed several key benefits:

Developer Productivity: Refit eliminated hundreds of lines of boilerplate HTTP client code. New developers can quickly understand and extend API integrations without learning complex HTTP client patterns.

Type Safety: Compile-time checking ensures that API contracts are maintained. When APIs change, the compiler immediately identifies affected clients.

Maintainability: Changes to API endpoints require updates only to interface definitions. The generated implementation handles all the underlying HTTP communication details.

Testing: Refit interfaces are easily mockable, enabling comprehensive unit testing of service interactions without complex HTTP mocking frameworks.

Consistency: All inter-service communication follows identical patterns, reducing cognitive load when working across different parts of the system.

Future Enhancements

Looking ahead, we're exploring several enhancements to our Refit implementation:

  1. Automatic Retry Policies: Integration with Polly for sophisticated retry and circuit breaker patterns
  2. Response Caching: Adding HTTP-level caching for appropriate GET operations
  3. Request/Response Logging: Enhanced observability for debugging and performance monitoring
  4. API Versioning: Support for multiple API versions within the same client interface

Conclusion

Refit has transformed how we handle inter-service communication in our platform. By providing a clean, type-safe abstraction over HTTP operations, it has enabled us to build a robust microservices architecture that is both maintainable and performant.

The combination of declarative interfaces, automatic serialization, integrated authentication, and seamless dependency injection makes Refit an invaluable tool for any .NET microservices architecture. Our implementation demonstrates how thoughtful architectural decisions can dramatically improve developer productivity while maintaining the flexibility and scalability that modern applications demand.

Whether you're building your first microservices system or looking to modernize an existing one, consider how Refit can simplify your HTTP client infrastructure and enable your team to focus on delivering business value rather than managing communication plumbing.