Entrevista técnica: Respondiendo las preguntas más comunes para Senior .NET (Parte 6)
Respondiendo las preguntas en la categoría rendimiento, escalabilidad y la nube!

Hey estimados devs, aquí continuaré dando respuesta a las preguntas planteadas en este artículo. El nivel está altísimo.

Categoría 5: Rendimiento, escalabilidad y Cloud

19. ¿Cómo escalarías una aplicación .NET en la nube (Azure, AWS) y por qué lo harías?

Para escalar una aplicación .NET en la nube, lo primero es diferenciar entre escalado vertical y escalado horizontal. El escalado vertical significa aumentar la capacidad de la misma máquina (más CPU, RAM, disco rápido), lo cual puede ser útil al inicio, pero tiene un límite físico. En cambio, el escalado horizontal consiste en añadir más instancias de la aplicación distribuidas detrás de un balanceador de carga, lo cual ofrece mayor resiliencia y capacidad de manejar más usuarios de forma sostenida.

Aquí tengo un artículo que habla de escalamiento 👌🏼

En la nube (ya sea Azure App Service o AWS Elastic Beanstalk/ECS), normalmente se comienza con autoescalado horizontal basado en métricas como el uso de CPU, la memoria o la cantidad de solicitudes por segundo.

Estimado dev, aquí te presento un ejemplo real: supongamos que tienes una API en .NET 8 que atiende a miles de clientes desde un eCommerce. En Azure, podrías desplegar tu API en un App Service con autoscale rules que lancen nuevas instancias cuando el CPU supere el 70% por más de 5 minutos, y reduzcan instancias en horas de baja demanda. También puedes desacoplar componentes usando Azure Service Bus o AWS SQS para procesar colas en segundo plano, evitando sobrecargar la API. Esto se hace no solo para soportar picos de tráfico, sino para asegurar alta disponibilidad (si una instancia falla, otras siguen funcionando) y costos optimizados (solo pagas por lo que usas).

20. ¿Cómo manejarías transacciones distribuidas en .NET?

En .NET, manejar transacciones distribuidas significa coordinar operaciones que afectan a más de un recurso al mismo tiempo (por ejemplo, una base de datos SQL y una cola de mensajes). Lo primero es evitar en lo posible las transacciones distribuidas clásicas (con DTC, Distributed Transaction Coordinator), porque generan acoplamiento fuerte y afectan la escalabilidad. En sistemas modernos, especialmente en la nube, lo recomendable es usar patrones de consistencia eventual como Outbox Pattern o Saga Pattern. Estos permiten garantizar que todos los pasos de una operación compleja se ejecuten de forma confiable, aunque no al mismo tiempo.

Ahora veamos un ejemplo real dev: Mira, imagina un sistema de pagos en eCommerce: primero registras la orden en SQL Server, luego envías un mensaje a un servicio de pagos en Azure Service Bus. Si haces ambas cosas dentro de una transacción distribuida clásica, dependes de DTC y limitas la escalabilidad en contenedores. En cambio, con el Outbox Pattern, escribes tanto la orden como el mensaje en la misma base de datos en una única transacción local. Luego, un proceso en segundo plano lee la tabla “Outbox” y publica el mensaje en el bus, asegurando que nunca se pierda. Si necesitas coordinar múltiples servicios (como pedidos, pagos, envíos), puedes usar el Saga Pattern, que define pasos con compensaciones en caso de fallo (por ejemplo, revertir el pago si no se puede reservar el envío).

Aquí te explico qué son las transacciones y en este artículo hablo acerca del CAP Theorem, eso te va a ampliar tu panorama muchísimo y entenderás cosas que la mayoría desconoce de este tema.

21. ¿Qué es el Circuit Breaker pattern y cómo lo implementarías en .NET? (Patrón clave de resiliencia y microservicios).

El Circuit Breaker pattern es un patrón de resiliencia que protege a tu aplicación cuando dependes de servicios externos (como APIs, bases de datos, colas entre otros) que pueden fallar o responder lento. Funciona como un interruptor: si detecta demasiados errores en poco tiempo, “abre el circuito” y bloquea temporalmente nuevas llamadas al servicio problemático. Esto evita saturar recursos con intentos fallidos y da tiempo a que el sistema externo se recupere. Una vez pasado el tiempo de espera, el circuit breaker permite algunas llamadas de prueba; si funcionan, “cierra el circuito” y vuelve a operar normalmente.

Circuit breaker tiene 3 estados: Open, Closed y half-open.

En .NET puedes implementarlo fácilmente con Polly, una librería popular para resilience policies. Por ejemplo, en un microservicio de pedidos que llama a un servicio de pagos, puedes definir un circuit breaker que se abra tras 5 fallos consecutivos y se mantenga abierto 30 segundos:

services.AddHttpClient("PaymentService")
    .AddPolicyHandler(HttpPolicyExtensions
        .HandleTransientHttpError() // Aquí manejas los errores 500, 408 y HttpRequestException
        .CircuitBreakerAsync(
            exceptionsAllowedBeforeBreaking: 5,
            durationOfBreak: TimeSpan.FromSeconds(30),
            onBreak: (outcome, breakDelay) => Console.WriteLine("Circuito abierto...!"),
            onReset: () => Console.WriteLine("Circuitocerrado!"),
            onHalfOpen: () => Console.WriteLine("Half-open...")
        ));

De esta forma, estimado dev proteges tu microservicio de caerse en cascada por culpa de un servicio lento o caído. Además, en la nube (con Azure o AWS) esto se combina muy bien con reintentos con backoff exponencial y timeouts, logrando aplicaciones bastante resilientes.

22. ¿Cómo diagnosticarías y resolverías un problema de memory leak en una aplicación .NET? (Habilidad crítica de debugging)

Dev, primero hay que definir qué es un memory leak en .NET.

Un memory leak es cuando tu aplicación sigue consumiendo memoria con objetos que ya no necesita, pero que nunca son liberados porque algo todavía los mantiene “vivos” en el programa. En .NET, aunque el Garbage Collector limpia la memoria automáticamente, si dejas referencias activas (por ejemplo un evento suscrito que nunca se desuscribe, o una lista donde nunca borras los elementos), esos objetos quedan atrapados y ocupan memoria de forma innecesaria.

Con el tiempo, el consumo de memoria empieza a crecer sin parar: la app se vuelve más lenta, el servidor necesita más recursos, e incluso puede terminar con un OutOfMemoryException.

Este comportamiento indeseado ocurre cuando objetos que ya no se necesitan siguen referenciados y no son liberados por el Garbage Collector (GC). Aunque el GC maneja automáticamente la memoria, las fugas pasan por malas prácticas como mantener eventos sin desuscribir, usar static innecesarios o acumular objetos en cachés sin límites.

Entonces qué puedes hacer como desarrollador capo que eres, bueno aquí te lo digo 🐿️

El primer paso es diagnosticar: habilitar métricas (Performance Counters, dotnet-counters) para ver el consumo de memoria administrada/no administrada, y luego usar herramientas como dotMemory de JetBrains, PerfView o Visual Studio Diagnostic Tools de Microsoft para tomar memory dumps y analizar qué objetos crecen sin liberar.

dotnet-counters es una herramienta de diagnóstico en línea de comandos que viene con el SDK de .NET. Sirve para monitorear métricas de rendimiento en tiempo real de una aplicación .NET que ya está corriendo, sin necesidad de detenerla o modificar su código. Es muy útil para detectar problemas de CPU, memoria, garbage collection, excepciones y throughput.

Por ejemplo: si notas que tu aplicación web en .NET 9 crece de 200 MB a 2 GB en pocas horas, tomas un dump y encuentras miles de instancias de HttpClient. Esto revela un mal uso (crear HttpClient por request en lugar de reutilizarlo).

Aquí la solución sería aplicar buenas prácticas: reutilizar HttpClient como static o con IHttpClientFactory, limpiar subscripciones a eventos, implementar correctamente IDisposable y establecer límites en estructuras en memoria (cachés con expiración).

En producción, además, se suele combinar con alertas en Azure Application Insights o AWS CloudWatch para detectar patrones de fuga antes de que la aplicación colapse.

23. Hablemos de rendimiento: ¿Qué técnicas usarías para optimizar una API .NET (caching, pooling, benchmarking)?

Y llegamos a la última pregunta de esta sección, uff sí que estamos entrando bastante en profundidad.

Para optimizar el rendimiento de una API en .NET lo primero es medir antes de optimizar. Esto implica usar benchmarking y profiling con herramientas como BenchmarkDotNet, dotnet-trace o Application Insights en la nube.

Una vez que identificas los cuellos de botella, aplicas técnicas específicas. Por ejemplo, el caching es clave: puedes usar IMemoryCache para almacenar resultados frecuentes en memoria (útil para catálogos pequeños) o Redis distribuido si necesitas compartir caché entre múltiples instancias en la nube. Esto evita recalcular o consultar la base de datos cada vez.

Otra técnica es el pooling y la reutilización de recursos. En .NET, por ejemplo, debes usar IHttpClientFactory en lugar de crear HttpClient por request, lo que evita fugas de sockets y mejora la latencia.

Lo mismo aplica para conexiones a la base de datos: usar un connection pool gestionado por ADO.NET en lugar de abrir/cerrar conexiones sin control. También es importante optimizar consultas con EF Core (usar proyecciones con Select, paginación, evitar Include innecesarios).

Finalmente, para mejorar la escalabilidad, puedes aplicar cancellation tokens en tus endpoints, limitar payloads grandes con compresión y usar programación asíncrona (async/await) para aprovechar mejor los recursos delservidor.

Te pongo un ejemplo real: en una API de eCommerce que devuelve productos más vendidos, puedes cachear el resultado en Redis durante 5 minutos y así evitar miles de consultas repetidas a SQL Server. Al mismo tiempo, usando BenchmarkDotNet, puedes comparar la latencia de serializar con System.Text.Json frente a Newtonsoft.Json y eliges la opción más rápida en producción.

Esto es todo por ahora, dev, en la siguiente entrada seguimos con la siguiente categoría! 🐿️🥳

¡Si esta entrada te ha gustado, compártela ah! 💪🏼

Créditos de imagen de portada: Basada en Foto de Gerald Schömbs en Unsplash

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *