Java Spring Boot + ASP.NET Core: Integration Patterns for Modern Applications

JNBridgePro — the fastest, easiest way to bridge Java and .NET in production. Generate proxies in minutes, call Java from C# (or C# from Java) with native syntax — trusted by enterprises worldwide. Learn more · Download free trial

Spring Boot and ASP.NET Core are the dominant web frameworks for Java and .NET respectively. Both are opinionated, production-ready, and share more similarities than most developers realize — dependency injection, middleware pipelines, configuration systems, and health check APIs. But when enterprises need both frameworks in the same architecture, the integration options are rarely discussed.

This guide covers the practical patterns for connecting Spring Boot and ASP.NET Core applications: from REST and gRPC communication to in-process bridging, shared authentication, distributed tracing, and the specific configuration needed to make these frameworks cooperate in production.

Why Spring Boot and ASP.NET Core End Up Together

Teams don’t usually plan to run both frameworks. It happens because:

  • Acquisitions: Company A (.NET shop) acquires Company B (Java shop). Now you have Spring Boot microservices talking to ASP.NET Core APIs.
  • Best-of-breed choices: The data engineering team chose Spring Boot for Kafka integration. The frontend team chose ASP.NET Core for the API gateway. Both are valid choices.
  • Legacy + Modern: A mature Spring Boot backend serves a new ASP.NET Core frontend built for a modern SPA.
  • Vendor libraries: A critical third-party SDK only ships as a Java JAR (Spring Boot compatible). Your application is ASP.NET Core.

Pattern 1: REST API Integration

The most common pattern: Spring Boot exposes REST endpoints, ASP.NET Core consumes them (or vice versa).

Integrating Spring Boot and ASP.NET Core? JNBridgePro adds a high-performance in-process option alongside REST and gRPC — download a free evaluation to benchmark all three patterns.

Spring Boot Side (API Provider)

// Spring Boot REST controller
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @Autowired
    private ProductService productService;
    
    @GetMapping("/{id}")
    public ResponseEntity<ProductDTO> getProduct(@PathVariable Long id) {
        return productService.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
    }
    
    @GetMapping
    public Page<ProductDTO> searchProducts(
            @RequestParam String query,
            Pageable pageable) {
        return productService.search(query, pageable);
    }
}

ASP.NET Core Side (API Consumer)

// ASP.NET Core typed HTTP client
public class ProductApiClient
{
    private readonly HttpClient _http;
    
    public ProductApiClient(HttpClient http)
    {
        _http = http;
    }
    
    public async Task<Product?> GetProductAsync(long id)
    {
        var response = await _http.GetAsync($"/api/products/{id}");
        if (!response.IsSuccessStatusCode) return null;
        return await response.Content.ReadFromJsonAsync<Product>();
    }
    
    public async Task<PagedResult<Product>> SearchAsync(
        string query, int page = 0, int size = 20)
    {
        var response = await _http.GetFromJsonAsync<PagedResult<Product>>(
            $"/api/products?query={query}&page={page}&size={size}");
        return response!;
    }
}

// Registration in Program.cs
builder.Services.AddHttpClient<ProductApiClient>(client =>
{
    client.BaseAddress = new Uri("http://spring-boot-service:8080");
    client.Timeout = TimeSpan.FromSeconds(10);
})
.AddTransientHttpErrorPolicy(p => 
    p.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(200 * attempt)))
.AddTransientHttpErrorPolicy(p => 
    p.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30)));

Key considerations:

  • Spring Boot’s pagination (Page<T>) uses different JSON structure than ASP.NET Core. Map accordingly.
  • Spring Boot returns dates as ISO-8601 by default (Jackson). ASP.NET Core’s System.Text.Json handles this correctly, but verify timezone handling.
  • Add Polly for retry and circuit breaker policies on the .NET side.
  • Spring Boot’s @Valid validation errors return 400 with a different structure than ASP.NET Core’s ProblemDetails. Normalize error handling.

Pattern 2: gRPC Integration

For higher-performance scenarios, gRPC provides better throughput than REST with strong typing via Protobuf:

// Shared .proto definition
syntax = "proto3";
package product;

service ProductService {
    rpc GetProduct (ProductRequest) returns (ProductResponse);
    rpc StreamPriceUpdates (PriceSubscription) returns (stream PriceUpdate);
}

message ProductRequest {
    int64 id = 1;
}

message ProductResponse {
    int64 id = 1;
    string name = 2;
    string description = 3;
    double price = 4;
    google.protobuf.Timestamp updated_at = 5;
}
// Spring Boot gRPC server (using grpc-spring-boot-starter)
@GrpcService
public class ProductGrpcService extends ProductServiceGrpc.ProductServiceImplBase {
    
    @Override
    public void getProduct(ProductRequest request, 
                          StreamObserver<ProductResponse> observer) {
        var product = productService.findById(request.getId());
        observer.onNext(toProto(product));
        observer.onCompleted();
    }
    
    @Override
    public void streamPriceUpdates(PriceSubscription request,
                                   StreamObserver<PriceUpdate> observer) {
        // Server-streaming: push real-time prices to .NET client
        priceService.subscribe(request.getSymbol(), update -> {
            observer.onNext(toPriceProto(update));
        });
    }
}

// ASP.NET Core gRPC client
public class ProductGrpcClient
{
    private readonly ProductService.ProductServiceClient _client;
    
    public async Task<Product> GetProductAsync(long id)
    {
        var response = await _client.GetProductAsync(
            new ProductRequest { Id = id });
        return Product.FromProto(response);
    }
    
    public async IAsyncEnumerable<PriceUpdate> StreamPricesAsync(
        string symbol, [EnumeratorCancellation] CancellationToken ct = default)
    {
        using var stream = _client.StreamPriceUpdates(
            new PriceSubscription { Symbol = symbol });
        
        await foreach (var update in stream.ResponseStream.ReadAllAsync(ct))
        {
            yield return PriceUpdate.FromProto(update);
        }
    }
}

gRPC’s server-streaming is particularly useful when Spring Boot pushes real-time data (prices, events, notifications) to ASP.NET Core consumers.

Pattern 3: In-Process Bridge Integration

When ASP.NET Core needs to use Java libraries directly — without running a separate Spring Boot service — an in-process bridge like JNBridgePro loads the JVM inside the ASP.NET Core process:

// ASP.NET Core service using Java Spring components directly
public class JavaRuleEngineService : IRuleEngine
{
    private readonly com.company.rules.RuleEngine _javaEngine;
    
    public JavaRuleEngineService()
    {
        // Bridge loads the Java rule engine in-process
        // No separate Spring Boot service needed
        _javaEngine = new com.company.rules.RuleEngine();
        _javaEngine.LoadRules("/rules/production.drl");
    }
    
    public RuleResult Evaluate(BusinessContext context)
    {
        var javaContext = ContextMapper.ToJava(context);
        var result = _javaEngine.Evaluate(javaContext);
        return RuleResult.FromJava(result);
    }
}

// Register in ASP.NET Core DI
builder.Services.AddSingleton<IRuleEngine, JavaRuleEngineService>();

When to use this: When you need a Java library (Drools rule engine, Apache Tika for document parsing, a proprietary Java SDK) but don’t want to deploy and maintain a separate Spring Boot service just to expose it.

Pattern 4: Event-Driven Integration via Kafka

For asynchronous workflows, Spring Boot and ASP.NET Core communicate through Apache Kafka (or RabbitMQ, Azure Service Bus):

// Spring Boot producer (using Spring Kafka)
@Service
public class OrderEventPublisher {
    @Autowired
    private KafkaTemplate<String, OrderEvent> kafka;
    
    public void publishOrderCreated(Order order) {
        kafka.send("order-events", order.getId(), 
            new OrderCreatedEvent(order));
    }
}

// ASP.NET Core consumer (using Confluent.Kafka)
public class OrderEventConsumer : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        using var consumer = new ConsumerBuilder<string, string>(_config)
            .Build();
        consumer.Subscribe("order-events");
        
        while (!ct.IsCancellationRequested)
        {
            var result = consumer.Consume(ct);
            var orderEvent = JsonSerializer.Deserialize<OrderEvent>(
                result.Message.Value);
            await ProcessOrderEvent(orderEvent!);
        }
    }
}

Schema compatibility tip: Use Confluent Schema Registry with Avro or Protobuf schemas. Both Spring Boot (via spring-kafka) and .NET (via Confluent.SchemaRegistry) support schema registry integration, ensuring both sides agree on message formats.

Shared Cross-Cutting Concerns

Authentication: Shared JWT Tokens

// Spring Boot: Validate JWT (using spring-security-oauth2-resource-server)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt.jwkSetUri("https://auth.company.com/.well-known/jwks")))
            .build();
    }
}

// ASP.NET Core: Validate same JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://auth.company.com";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = "https://auth.company.com",
            ValidateAudience = true,
            ValidAudience = "api"
        };
    });

Both frameworks validate the same JWT tokens from the same identity provider. Users authenticate once and access both Spring Boot and ASP.NET Core services seamlessly.

Distributed Tracing with OpenTelemetry

// Spring Boot: OpenTelemetry auto-instrumentation
// application.yml
otel:
  service:
    name: spring-product-service
  exporter:
    otlp:
      endpoint: http://jaeger:4317

// ASP.NET Core: OpenTelemetry SDK
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .SetResourceBuilder(ResourceBuilder.CreateDefault()
            .AddService("dotnet-api-gateway"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddGrpcClientInstrumentation()
        .AddOtlpExporter(o => o.Endpoint = new Uri("http://jaeger:4317")));

With both frameworks exporting to the same OpenTelemetry collector, you get unified traces showing requests flowing from ASP.NET Core → Spring Boot (or vice versa) with full timing breakdowns.

Shared Configuration via Consul/etcd

// Spring Boot: Read from Consul
// bootstrap.yml
spring:
  cloud:
    consul:
      config:
        prefix: config
        default-context: shared

// ASP.NET Core: Read from same Consul
builder.Configuration.AddConsul("config/shared", options =>
{
    options.ConsulConfigurationOptions = co =>
    {
        co.Address = new Uri("http://consul:8500");
    };
    options.ReloadOnChange = true;
});

Choosing the Right Pattern

ScenarioRecommended PatternWhy
Separate teams, separate deploymentsREST or gRPCClear contract boundary, independent scaling
Real-time data streaminggRPC server-streaming or KafkaEfficient push model
Need Java library in .NET appIn-process bridge (JNBridgePro)No separate service to deploy
Async event-driven workflowKafka/RabbitMQDecoupled, resilient, scalable
High-frequency calls (>1K/sec)gRPC or JNBridgeProREST too slow at high volume
Migration in progressIn-process bridgeTemporary integration without throwaway APIs

Framework Feature Mapping

For developers working across both frameworks, here’s how the core concepts map:

ConceptSpring BootASP.NET Core
DI ContainerSpring IoC / @Autowiredbuilder.Services / [FromServices]
MiddlewareFilters / Interceptorsapp.Use() middleware pipeline
Configurationapplication.yml / @Valueappsettings.json / IConfiguration
Health ChecksActuator /healthMapHealthChecks()
MetricsMicrometerSystem.Diagnostics.Metrics
ORMJPA / HibernateEntity Framework Core
Validation@Valid / Bean ValidationDataAnnotations / FluentValidation
Background Jobs@Scheduled / Spring BatchBackgroundService / Hangfire
API DocsSpringDoc / SwaggerSwashbuckle / NSwag

Conclusion

Spring Boot and ASP.NET Core are more alike than different. When they need to work together, the integration pattern depends on your coupling requirements, performance needs, and team structure. REST works for most cases, gRPC for performance-sensitive paths, Kafka for event-driven architectures, and an in-process bridge for direct Java library access from .NET.

The key is choosing the right pattern for each integration point — and it’s perfectly valid to use multiple patterns in the same system. JNBridgePro is particularly useful during migration periods and when ASP.NET Core applications need direct access to Java libraries without the overhead of a separate service.

Need to integrate Spring Boot with ASP.NET Core? Try JNBridgePro for direct Java/.NET integration, or explore our blog for more architecture patterns.

Related Articles