📞 gRPC, Versionado de APIs y Database per Service
gRPC — Comunicación de alto rendimiento
gRPC es ideal para comunicación interna entre microservicios donde importa la latencia y el tipado estricto del contrato.
REST vs gRPC:
REST: JSON sobre HTTP/1.1 → texto, verbose, sin contrato forzado
gRPC: Protobuf sobre HTTP/2 → binario, compacto, contrato en .proto
Ventajas de gRPC en microservicios:
✅ 5-10x más eficiente que JSON en tamaño de payload
✅ HTTP/2: multiplexing, headers comprimidos
✅ Streaming bidireccional (ideal para eventos en tiempo real)
✅ Contrato explícito en .proto → genera código cliente/servidor
✅ Tipado estricto (no hay sorpresas de campos faltantes)
Desventajas:
❌ No legible por humanos (necesitas herramientas para inspeccionar)
❌ No soportado nativamente por navegadores (necesita grpc-web)
❌ Más setup inicial
Definición del contrato (.proto)
// protos/inventario.proto
syntax = "proto3";
option csharp_namespace = "MiApp.Inventario";
package inventario;
service InventarioService {
rpc ObtenerStock (StockRequest) returns (StockResponse);
rpc ReservarStock (ReservaRequest) returns (ReservaResponse);
// Streaming: emitir actualizaciones de stock en tiempo real
rpc MonitorearStock (StockRequest) returns (stream StockResponse);
}
message StockRequest {
string producto_id = 1;
}
message StockResponse {
string producto_id = 1;
int32 cantidad = 2;
bool disponible = 3;
}
message ReservaRequest {
string producto_id = 1;
int32 cantidad = 2;
string pedido_id = 3;
}
message ReservaResponse {
bool exitoso = 1;
string mensaje = 2;
}
Servidor gRPC en ASP.NET Core
// InventarioGrpcService.cs
public class InventarioGrpcService : InventarioService.InventarioServiceBase
{
private readonly IInventarioRepository _repo;
public override async Task<StockResponse> ObtenerStock(
StockRequest request, ServerCallContext context)
{
var stock = await _repo.GetByProductoIdAsync(request.ProductoId);
if (stock is null)
throw new RpcException(new Status(StatusCode.NotFound,
$"Producto {request.ProductoId} no encontrado"));
return new StockResponse
{
ProductoId = stock.ProductoId,
Cantidad = stock.Cantidad,
Disponible = stock.Cantidad > 0
};
}
// Streaming: emite actualizaciones cada vez que el stock cambia
public override async Task MonitorearStock(
StockRequest request,
IServerStreamWriter<StockResponse> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var stock = await _repo.GetByProductoIdAsync(request.ProductoId);
await responseStream.WriteAsync(new StockResponse
{
ProductoId = stock.ProductoId,
Cantidad = stock.Cantidad,
Disponible = stock.Cantidad > 0
});
await Task.Delay(TimeSpan.FromSeconds(5), context.CancellationToken);
}
}
}
// Program.cs
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
app.MapGrpcService<InventarioGrpcService>();
Cliente gRPC (en el servicio Pedidos)
// Registro del cliente con interceptors y resiliencia
builder.Services.AddGrpcClient<InventarioService.InventarioServiceClient>(options =>
{
options.Address = new Uri("https://inventario-service:443");
})
.AddCallCredentials(async (context, metadata) =>
{
// Autenticación service-to-service
var token = await _tokenService.GetServiceTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
// En Kubernetes con mTLS, deshabilitar validación del cert del servicio interno
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
// Uso en el servicio de Pedidos
public class PedidoService
{
private readonly InventarioService.InventarioServiceClient _inventario;
public async Task<bool> ProcesarPedidoAsync(Pedido pedido)
{
try
{
var reserva = await _inventario.ReservarStockAsync(new ReservaRequest
{
ProductoId = pedido.ProductoId,
Cantidad = pedido.Cantidad,
PedidoId = pedido.Id.ToString()
});
return reserva.Exitoso;
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.Unavailable)
{
// El servicio de inventario no está disponible
throw new ServicioNoDisponibleException("Inventario", ex);
}
}
}
Versionado de APIs entre microservicios
Estrategias de versionado
1. URL VERSIONING — el más explícito y común
GET /api/v1/productos
GET /api/v2/productos
Pros: visible, cacheable, fácil de enrutar en el API Gateway
Cons: "contamina" la URL (REST puristas lo critican)
2. HEADER VERSIONING — más "limpio" según REST
GET /api/productos
Header: Api-Version: 2.0
Pros: URL limpia
Cons: no cacheable por CDN, no visible en la URL
3. CONTENT NEGOTIATION (Accept header)
GET /api/productos
Accept: application/vnd.miapp.v2+json
Pros: es el más "RESTful"
Cons: verboso, difícil de testear desde el navegador
4. QUERY STRING — evitar en APIs públicas
GET /api/productos?version=2
Implementación en ASP.NET Core
// Instalar: dotnet add package Asp.Versioning.Mvc
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // Agrega headers api-supported-versions
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // /api/v1/...
new HeaderApiVersionReader("Api-Version"), // Header: Api-Version: 1.0
new QueryStringApiVersionReader("v") // ?v=1.0 (fallback)
);
});
// V1 — contrato original
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/productos")]
public class ProductosV1Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new[] { new ProductoDtoV1 { Id = 1, Nombre = "Laptop" } });
}
// V2 — contrato extendido (nuevo campo, nunca quitar campos de V1)
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/productos")]
public class ProductosV2Controller : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok(new[] { new ProductoDtoV2
{
Id = 1, Nombre = "Laptop",
Categoria = "Electrónica", // Campo nuevo en V2
Precio = 999.99m // Campo nuevo en V2
}});
}
// Deprecar una versión (avisa a los clientes con headers)
[ApiVersion("1.0", Deprecated = true)]
Versionado de mensajes/eventos en el bus
// Estrategia: envelope con versión explícita
public class EventEnvelope<T>
{
public string EventType { get; set; } = typeof(T).Name;
public string Version { get; set; } = "1.0";
public DateTime OccurredAt { get; set; } = DateTime.UtcNow;
public T Payload { get; set; } = default!;
}
// Consumidor tolerante (Tolerant Reader pattern):
// Acepta V1 y V2, los campos nuevos son opcionales
public class PedidoConfirmadoConsumer
: IConsumer<EventEnvelope<PedidoConfirmadoV1>>,
IConsumer<EventEnvelope<PedidoConfirmadoV2>>
{
public async Task Consume(ConsumeContext<EventEnvelope<PedidoConfirmadoV1>> ctx)
=> await ProcesarAsync(ctx.Message.Payload.PedidoId, null);
public async Task Consume(ConsumeContext<EventEnvelope<PedidoConfirmadoV2>> ctx)
=> await ProcesarAsync(ctx.Message.Payload.PedidoId, ctx.Message.Payload.MetodoEnvio);
private Task ProcesarAsync(int pedidoId, string? metodoEnvio) { /*...*/ }
}
Database per Service
El principio más importante (y más violado) de los microservicios.
❌ ANTI-PATRÓN: Base de datos compartida
─────────────────────────────────────
Servicio Pedidos ──→ ┐
Servicio Inventario ─┤──→ DB compartida
Servicio Usuarios ───┘
Problemas:
- Coupling fuerte: cambiar el schema afecta a todos
- No puedes escalar las bases de datos independientemente
- Si la DB cae, caen todos los servicios
- Los servicios se pueden "llamar" entre sí vía la DB (caos)
✅ CORRECTO: Database per Service
──────────────────────────────
Servicio Pedidos ──→ DB Pedidos (SQL Server)
Servicio Inventario ──→ DB Inventario (PostgreSQL)
Servicio Catálogo ──→ DB Catálogo (MongoDB)
Servicio Búsqueda ──→ Elasticsearch
Ventajas:
+ Cada servicio elige el motor más adecuado para sus datos
+ Escalan independientemente
+ Fallo de una DB no afecta a otras
+ Los contratos son explícitos (API/eventos, no SQL)
Cómo manejar queries cross-service sin DB compartida
Problema: necesito mostrar "historial de pedidos con datos del usuario"
pero pedidos y usuarios son servicios diferentes.
Opción 1: API Composition (el API Gateway junta la respuesta)
Gateway ──→ GET /pedidos?userId=123 → [pedidoId: 1, userId: 123, ...]
Gateway ──→ GET /usuarios/123 → { nombre: "Ana", email: ... }
Gateway combina y devuelve al cliente
Opción 2: CQRS + Read Model (el más escalable)
Cada vez que se crea un pedido (evento), el servicio de Read Models
consume el evento y guarda una vista desnormalizada:
{ pedidoId, userId, nombreUsuario, emailUsuario, productos, total }
Las queries leen de esta vista desnormalizada, sin JOINs cross-service.