Una de las preguntas más frecuentes en entrevistas es "¿cuándo usarías X en lugar de Y?". Esta página es tu referencia rápida de los trade-offs más preguntados.
La respuesta siempre empieza con "depende"
Un candidato Senior no da respuestas absolutas. Menciona los contextos donde cada opción gana. Eso es lo que el entrevistador quiere escuchar.
Métodos que frecuentemente retornan síncronamente (cache hits)
Complejidad
Simple y predecible
Más restrictivo, fácil de usar mal
Regla práctica: Usa Task<T> por defecto. Cambia a ValueTask<T> solo cuando hay evidencia de presión en el GC por alocaciones de Task en paths de alta frecuencia.
Apps CRUD, dominios complejos, equipos que priorizan velocidad
Reportes, queries complejas con JOINs, hot paths críticos de performance
Regla práctica: EF Core para el 90% del CRUD. Dapper (o EF + FromSqlRaw) para las queries que EF genera mal o para reporting.
// Combinar ambos en el mismo proyecto public class ProductoRepository { private readonly AppDbContext _efContext; private readonly IDbConnection _dapperConn; // CRUD con EF Core public Task<Producto?> GetByIdAsync(int id) => _efContext.Productos.AsNoTracking().FirstOrDefaultAsync(p => p.Id == id); // Reporte complejo con Dapper public Task<IEnumerable<ReporteVentasDto>> GetReporteVentasAsync(DateTime desde, DateTime hasta) => _dapperConn.QueryAsync<ReporteVentasDto>(""" SELECT c.Nombre, SUM(p.Total) as TotalVentas, COUNT(*) as CantidadPedidos FROM Pedidos p JOIN Clientes c ON p.ClienteId = c.Id WHERE p.Fecha BETWEEN @Desde AND @Hasta GROUP BY c.Nombre ORDER BY TotalVentas DESC """, new { Desde = desde, Hasta = hasta }); }
// ✅ Singleton: sin estado mutable o thread-safe builder.Services.AddSingleton<IConfiguration>(); builder.Services.AddSingleton<IMemoryCache, MemoryCache>(); builder.Services.AddSingleton<IHttpClientFactory>(); // ya es singleton por default // ✅ Scoped: estado por request builder.Services.AddScoped<AppDbContext>(); builder.Services.AddScoped<IProductoRepository, ProductoRepository>(); builder.Services.AddScoped<ICurrentUser, CurrentUserService>(); // ✅ Transient: sin estado builder.Services.AddTransient<IEmailValidator, EmailValidator>(); builder.Services.AddTransient<IPasswordHasher, Argon2PasswordHasher>();
Captive dependency
Si un Singleton inyecta un Scoped, el Scoped queda "capturado" y vive tanto como el Singleton — pierde su semántica de request. El DI container lanza excepción en desarrollo. Regla: un servicio nunca puede depender de uno con vida más corta.
// ✅ async/await para I/O — libera el hilo mientras espera public async Task<Producto?> GetProductoAsync(int id) { return await _db.Productos.FindAsync(id); // hilo libre mientras espera la BD } // ✅ Task.Run para CPU-bound — mueve el trabajo al ThreadPool public async Task<byte[]> GenerarReportePdfAsync(ReporteData data) { // Operación CPU-intensiva — no queremos bloquear el request thread return await Task.Run(() => PdfGenerator.Generate(data)); } // ❌ Incorrecto: Task.Run para I/O (desperdicia un hilo extra) public async Task<Producto?> GetProductoMal(int id) { return await Task.Run(() => _db.Productos.Find(id)); // usa un hilo para esperar otro } // ❌ Incorrecto: async/await para CPU (bloquea el hilo igual) public async Task<byte[]> GenerarPdfMal(ReporteData data) { await Task.Delay(0); // no ayuda — el cómputo sigue en el mismo hilo return PdfGenerator.Generate(data); }
IHostedService vs BackgroundService vs Worker Service
IHostedService
BackgroundService
Worker Service
Qué es
Interfaz de bajo nivel
Clase base abstracta sobre IHostedService
Template de proyecto
Implementación
StartAsync y StopAsync manuales
Solo sobreescribir ExecuteAsync
Usa BackgroundService por defecto
Cuándo usar
Control total del ciclo de vida
99% de los casos de background tasks
Punto de entrada para workers standalone
// BackgroundService: solo implementar ExecuteAsync public class ProcesadorColaService : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var mensaje = await _cola.RecibirAsync(stoppingToken); await ProcesarAsync(mensaje, stoppingToken); } } }
Apps públicas con SEO, e-commerce, marketing sites
Apps internas (dashboards, admin), detrás de login, alta interactividad
Regla: Si la página necesita SEO o se beneficia de pre-rendering → Next.js. Si es una app interna o dashboard donde el SEO no importa → Vite + React es más simple.
useEffect vs useLayoutEffect vs useSyncExternalStore
useEffect
useLayoutEffect
useSyncExternalStore
Cuándo corre
Después del paint del navegador
Antes del paint (síncrono)
Suscripción a stores externos
Bloquea el render
No
Sí
No
Cuándo usar
Fetch de datos, subscriptions, cleanup
Medir DOM, evitar flash visual
Integrar con stores de terceros
// useLayoutEffect: medir un elemento ANTES de que el usuario lo vea function Tooltip({ texto, targetRef }) { const [posicion, setPosicion] = useState({ top: 0, left: 0 }); useLayoutEffect(() => { // corre antes del paint — sin flicker const rect = targetRef.current.getBoundingClientRect(); setPosicion({ top: rect.bottom, left: rect.left }); }, []); return <div style={posicion}>{texto}</div>; }
Apps web tradicionales, cuando la revocación inmediata es crítica
Regla: JWT para APIs y microservicios donde la escalabilidad importa. Sessions cuando necesitás revocar tokens inmediatamente (bancas, admin). Para SPAs con backend propio, cookies HttpOnly son más seguras que localStorage.