Saltar al contenido principal

⚖️ Comparativas — ¿Cuándo usar X vs Y?

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.


.NET y C#

Task<T> vs ValueTask<T>

Task<T>ValueTask<T>
AlocaciónSiempre aloca en heapNo aloca cuando el resultado es síncrono
Awaitable múltiple✅ Se puede await N veces❌ Solo una vez
Conversión a TaskN/A.AsTask() tiene costo
Caso de usoDefault — operaciones que casi siempre son asyncMétodos que frecuentemente retornan síncronamente (cache hits)
ComplejidadSimple y predecibleMá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.


IEnumerable<T> vs IQueryable<T>

IEnumerable<T>IQueryable<T>
Dónde ejecutaEn memoria (.NET)En el servidor (SQL, etc.)
FiltrosSe aplican después de cargar todoSe traducen a SQL/query
Uso con EF CoreCarga toda la tabla, luego filtraGenera query SQL óptima
Cuándo usarColecciones en memoria, resultados ya cargadosQueries a BD que deben optimizarse
// ❌ Carga TODOS los productos, luego filtra en memoria
context.Productos.ToList().Where(p => p.Precio > 100);

// ✅ Genera: SELECT * FROM Productos WHERE Precio > 100
context.Productos.Where(p => p.Precio > 100).ToList();

string vs StringBuilder

stringStringBuilder
InmutabilidadInmutable — cada + crea un nuevo objetoMutable — modifica el buffer interno
RendimientoO(n²) para concatenación en bucleO(n) para concatenación en bucle
Cuando usarConcatenaciones simples (< 5-10 strings)Bucles, construcción dinámica de strings largos
LegibilidadMejor para casos simplesVerboso para casos simples

Regla práctica: string para todo lo normal. StringBuilder cuando concatenas dentro de un bucle o construyes strings dinámicamente de partes.


record vs class

recordclass
IgualdadPor valor (compara propiedades)Por referencia (misma instancia)
InmutabilidadPor defecto con init-only propertiesMutable por defecto
ToString()Generado automáticamenteHeredado de object (poco útil)
Cuándo usarDTOs, Value Objects, resultados de queriesEntidades con identidad, servicios, repositorios

Base de Datos

SQL vs NoSQL

SQL (Relacional)NoSQL
Modelo de datosTablas con esquema fijoDocumentos, key-value, grafos, columnar
TransaccionesACID completoEventual consistency (generalmente)
EscalabilidadVertical (scale-up), sharding complejoHorizontal nativo (scale-out)
Queries complejasJOINs, agregaciones, window functionsLimitado (varía por tipo)
EsquemaRígido — migraciones necesariasFlexible — fácil de cambiar
Cuándo usarDatos relacionales, transacciones importantes, reportingAlto throughput, datos semi-estructurados, escala masiva

Regla práctica: Empieza con SQL. Cambia a NoSQL cuando tengas un problema de escala o un modelo de datos que no encaja en tablas.


DELETE vs TRUNCATE vs DROP

DELETETRUNCATEDROP
¿Qué elimina?Filas (con WHERE opcional)Todas las filasTabla completa
Rollback✅ Sí❌ No (en la mayoría de motores)❌ No
VelocidadLento (log por cada fila)Rápido (deallocate pages)Instantáneo
WHERE✅ Sí❌ No❌ No
Reinicia identity❌ No✅ SíN/A
Cuándo usarBorrar filas específicasVaciar una tabla completa rápidoEliminar la estructura de la tabla

Offset Pagination vs Cursor Pagination

Offset (SKIP/OFFSET)Cursor (basada en ID)
RendimientoO(n) — empeora con páginas profundasO(log n) — constante gracias al índice
ConsistenciaPuede saltear/duplicar si se insertan registrosSiempre consistente
Salto a página✅ Cualquier página arbitraria❌ Solo secuencial
ImplementaciónSimpleMás compleja
Cuándo usarReportes con datos estáticos, pocas páginasFeeds en tiempo real, datasets grandes

Read Replica vs Cache

Read ReplicaCache (Redis)
Latencia~5-20ms (red + query)<1ms (memoria)
ConsistenciaEventual (lag de replicación)Puede quedar stale (TTL)
Tipo de datosCualquier query SQLDatos serializables
InvalidaciónAutomática por replicaciónManual o por TTL
Cuándo usarQueries complejas, reportes, offload de la primaryDatos frecuentemente leídos, resultados de queries costosas

Arquitectura

Microservicios vs Monolito Modular

MicroserviciosMonolito Modular
DeployabilidadDeploy independiente por servicioDeploy del todo junto
EscalabilidadEscalar servicios individualmenteEscalar todo junto
Complejidad operacionalAlta (K8s, service mesh, distributed tracing)Baja
LatenciaRed entre serviciosLlamadas en proceso (nanosegundos)
ConsistenciaEventual consistency (difícil)Transacciones ACID (fácil)
EquipoEquipos grandes, múltiples teamsEquipos pequeños-medianos
Cuándo usarSistema maduro, escala diferenciada, múltiples equiposMVP, dominios no bien entendidos, equipos pequeños

Regla: Empieza con un monolito modular bien diseñado. Extrae microservicios cuando tengas un problema real de escala o de independencia de deploy.


REST vs gRPC vs GraphQL

RESTgRPCGraphQL
ProtocoloHTTP/1.1 con JSONHTTP/2 con Protocol BuffersHTTP con JSON
RendimientoMedioAlto (binary, streaming)Medio
FlexibilidadFija por endpointFija por contratoCliente elige los campos
ToolingExcelente (Swagger, etc.)Bueno pero más complejoBueno (Apollo, etc.)
StreamingNo nativo✅ Bidireccional✅ Subscriptions
Cuándo usarAPIs públicas, web, móvilComunicación interna entre microserviciosAPIs con clientes que necesitan flexibilidad (dashboards, móvil con BW limitado)

CQRS simple vs CQRS con DB separadas

CQRS lógico (misma DB)CQRS con DB separadas
ComplejidadBaja-mediaAlta
ConsistenciaInmediataEventual (replicación)
Rendimiento de lecturaMedio (misma DB)Alto (DB optimizada para lecturas)
Cuándo usarQuieres separar responsabilidades sin infraestructura extraReads y writes tienen perfiles de carga muy distintos, necesitas optimización extrema

Repository Pattern vs DbContext directo

RepositoryDbContext directo
AbstracciónAlta — oculta EF CoreBaja — EF Core es visible
TestabilidadFácil mockear con interfacesRequiere InMemory/TestContainers
IQueryableGeneralmente ocultoAccesible — composición de queries
OverheadUna capa adicionalNinguno
Cuándo usarTesting con mocks, posibilidad real de cambiar ORM, Clean ArchitectureProyectos simples, cuando IQueryable tiene valor, cuando el equipo conoce bien EF Core

Mensajería

RabbitMQ vs Kafka vs Azure Service Bus

RabbitMQKafkaAzure Service Bus
ModeloPush (broker → consumer)Pull (consumer lee particiones)Push (broker → consumer)
ThroughputAltoMuy alto (millones/s)Medio-alto
RetenciónHasta que se consumeConfigurable (días, semanas, para siempre)Hasta que se consume
Replay❌ No nativo✅ Sí❌ No nativo
RoutingMuy flexible (exchanges, bindings)Simple (topic + particiones)Medio (topics, subscriptions, filtros)
OrderingPor queuePor particiónPor session
Cuándo usarRouting complejo, RPC, queues de trabajoEvent sourcing, log de auditoría, alto throughput, streamingApps Azure, cuando prefieres PaaS sin gestión

Frontend

useState vs useReducer

useStateuseReducer
ComplejidadSimpleMás verboso
Múltiples sub-estadosVarios useState separadosUn solo objeto de estado
TransicionesDirectasExplícitas vía actions
Testing del estadoDifícil de testear aisladoReducer es función pura — fácil de testear
Cuándo usarEstado simple (boolean, string, number)3+ sub-estados relacionados, transiciones complejas

Redux Toolkit vs Zustand vs Context API

Redux ToolkitZustandContext API
Bundle size~50KB~1KB0 (nativo)
BoilerplateMedio (reducido vs Redux puro)MínimoMínimo
DevTools✅ Excelentes✅ Disponibles❌ Ninguno
PerformanceExcelente con selectoresExcelenteRe-renders innecesarios si no se optimiza
Cuándo usarApps grandes, estado complejo, múltiples equiposApps medianas, estado simple-medioEstado de poca frecuencia de cambio, tema/locale, auth

React Query vs SWR vs fetch manual

React QuerySWRfetch manual + useState
Cache✅ Potente✅ Sí❌ Manual
Revalidación✅ On focus, interval, etc.✅ Sí❌ Manual
Mutations✅ Con optimistic updates✅ Básico❌ Manual
Paginación✅ Infinite scroll, cursors✅ Básico❌ Manual
Bundle size~40KB~4KB0
Cuándo usarApps con mucha data fetchingApps medianas con SWR sufficingMuy pocas requests, máximo control

Caching

In-Memory Cache vs Redis

IMemoryCache (in-process)Redis (distribuido)
Latencia~nanosegundos (RAM local)~1-5ms (red)
ConsistenciaSolo ese procesoCompartido entre instancias
Multi-instancia❌ Cada instancia tiene su caché✅ Caché compartido
PersistenciaNo (se pierde al reiniciar)Opcional (AOF / RDB)
Cuándo usarSingle-instance, dev, datos de configuraciónLoad balancer, múltiples pods, sesiones

Cache-Aside vs Write-Through vs Write-Behind

Cache-AsideWrite-ThroughWrite-Behind
Quien actualizaLa appEl caché (synced)El caché (async)
Cache missCarga de DB y almacenaNo hay miss (siempre sync)No hay miss
ConsistenciaEventualInmediataEventual
EscriturasDirectamente a DBSynced con cachéAsync (riesgo de pérdida)
Cuándo usarDefault, más simpleDatos críticos sin tolerancia a stalenessAlto throughput de escritura, pérdida tolerable

.NET y C# — avanzado

EF Core vs Dapper

EF CoreDapper
TipoORM completoMicro-ORM (wrapper de ADO.NET)
Curva de aprendizajeAltaBaja
ProductividadAlta (migrations, change tracking, LINQ)Media (SQL manual)
Control del SQLMedio (genera SQL automático)Total (escribís el SQL)
PerformanceBuena — mejora con AsNoTracking, proyeccionesExcelente — zero overhead
Migrations✅ Integradas❌ Manual (Flyway, DbUp, etc.)
Queries complejasA veces genera SQL ineficienteSQL exacto que necesitás
Cuándo usarApps CRUD, dominios complejos, equipos que priorizan velocidadReportes, 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 vs Scoped vs Transient

La decisión más frecuente al registrar servicios en el DI container de .NET.

SingletonScopedTransient
Instancias1 para toda la app1 por request HTTPNueva en cada inyección
Vida útilHasta que la app terminaHasta que termina el requestInmediata
Thread safety⚠️ Debe ser thread-safeSolo dentro del requestNo aplica
EstadoCompartido entre todosCompartido dentro del requestNinguno (nueva instancia)
Cuándo usarConfig, caché in-memory, HTTP clients, conexiones costosasDbContext, servicios de negocio, repositoriosHelpers stateless, factories
// ✅ 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 vs Task.Run

Confusión frecuente en entrevistas.

async/awaitTask.Run
PropósitoI/O-bound async (no bloquea el hilo)CPU-bound (corre en ThreadPool)
HiloLibera el hilo mientras espera I/OUsa un hilo del ThreadPool
Cuándo usarDB queries, HTTP calls, file I/OCálculos pesados, operaciones CPU intensivas
// ✅ 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

IHostedServiceBackgroundServiceWorker Service
Qué esInterfaz de bajo nivelClase base abstracta sobre IHostedServiceTemplate de proyecto
ImplementaciónStartAsync y StopAsync manualesSolo sobreescribir ExecuteAsyncUsa BackgroundService por defecto
Cuándo usarControl total del ciclo de vida99% de los casos de background tasksPunto 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);
}
}
}

Frontend avanzado

Next.js (App Router) vs Vite + React (SPA)

Next.js App RouterVite + React (SPA pura)
RenderingSSR, SSG, ISR, RSCSolo CSR (cliente)
SEO✅ Excelente (HTML pre-renderizado)❌ Malo sin configuración extra
Bundle inicialMás pequeño (Server Components)Completo
RoutingFile-based (carpetas)Manual (React Router)
Backend integrado✅ API Routes, Server Actions❌ Necesitás API separada
ComplejidadAlta (Server/Client components, caché)Baja-media
Cuándo usarApps públicas con SEO, e-commerce, marketing sitesApps 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

useEffectuseLayoutEffectuseSyncExternalStore
Cuándo correDespués del paint del navegadorAntes del paint (síncrono)Suscripción a stores externos
Bloquea el renderNoNo
Cuándo usarFetch de datos, subscriptions, cleanupMedir DOM, evitar flash visualIntegrar 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>;
}

Seguridad y autenticación

JWT vs Session Cookies

JWT (stateless)Session Cookies (stateful)
Estado en servidorNingunoSesión guardada (memoria/Redis)
EscalabilidadTrivial (cualquier instancia valida)Requiere session store compartido
RevocaciónDifícil (hasta que expira)Inmediata (borrar la sesión)
CSRFNo vulnerable (no en cookies)Vulnerable sin anti-CSRF token
XSSVulnerable si se guarda en localStorageMitigable con HttpOnly cookie
Cuándo usarAPIs stateless, microservicios, mobile, SPAsApps 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.


OAuth 2.0 vs API Keys

OAuth 2.0 / OIDCAPI Keys
Delegación✅ El usuario autoriza sin compartir credenciales❌ La key tiene acceso directo
Expiración✅ Tokens de corta vida❌ Generalmente permanentes
Revocación✅ Por token o refresh tokenRevocar la key entera
Scopes✅ Acceso granular por scope❌ Todo o nada
ComplejidadAltaBaja
Cuándo usarAcceso en nombre de un usuario, third-party integrationsServer-to-server, webhooks, cuando no hay usuario involucrado

Rate Limiting

Fixed Window vs Sliding Window vs Token Bucket

Fixed WindowSliding WindowToken Bucket
Burst al borde❌ Posible (2× el límite)✅ Eliminado✅ Controlado
MemoriaBajaAlta (timestamps por request)Baja
RecuperaciónAl inicio de la ventanaContinuaGradual
Burst deliberado❌ No permite❌ No permite✅ Permite (hasta capacidad)
Cuándo usarLímites simples de API públicaAPIs donde el burst en bordes es problemaAPIs que toleran picos ocasionales

Multi-tenancy

Database per Tenant vs Schema per Tenant vs Row-level

Database per TenantSchema per TenantRow-level
AislamientoMáximoAltoBajo
CostoAlto (N DBs)MedioBajo (1 DB)
MigrationsComplejo (correr en N DBs)ComplejoSimple
Riesgo de data leakMínimoBajoBug = exposición total
Cuándo usarRegulación estricta (HIPAA, GDPR)Balance seguridad/costoStartups, muchos tenants pequeños

DevOps

Docker Compose vs Kubernetes

Docker ComposeKubernetes
PropósitoDesarrollo local, testingProducción, orquestación
Auto-healing❌ No✅ Reinicia pods caídos
ScalingManual✅ HPA automático
Load balancingBásico✅ Nativo (Service)
Secretos.env filesSecrets, External Secrets
Curva de aprendizajeBajaAlta
Cuándo usarDev local, CI, staging simpleProducción, múltiples réplicas, escala

Regla: Docker Compose para desarrollo y CI. Kubernetes para producción cuando necesitás escala, self-healing o deploys complejos.


GitHub Actions vs Azure DevOps vs GitLab CI

GitHub ActionsAzure DevOpsGitLab CI
IntegraciónGitHub nativoAzure ecosystemGitLab nativo
Marketplace✅ EnormeMedioMedio
Self-hosted runners✅ Sí✅ Sí✅ Sí
Cost (public repos)GratisGratis (5 usuarios)Gratis
Cuándo usarRepos en GitHubEmpresa con Azure/ADO ya instaladoRepos en GitLab, on-premise