La entrevista se juega en UNA pregunta, y va primera.
Escribir a mano un JOIN de 3 tablas con filtro por fecha y paginación, y explicar la transacción. Tu CV no menciona SQL en ningún lado y para ellos el acceso a datos escrito a mano es "la mitad del trabajo". Si sale con soltura → subes al primer puesto de Full-Stack. Si no → deciden si te forman o te mueven a Front End Angular.
Riesgo operativo #1: el toolchain.
Confirmaste "equipo propio" pero no el entorno. Si llegas sin SQL Server + .NET SDK + Angular CLI montados y probados, la prueba técnica no se puede correr. Ver checklist en la pestaña Plan 5 días → Día 5. Esto es lo primero que debes cerrar.
| # | Foco | Por qué | Dónde |
|---|---|---|---|
| 1 | Joins + paginación (OFFSET/FETCH y keyset) + transacciones en SQL Server | Es la pregunta que decide | Pestaña SQL |
| 2 | Acceso a datos "a mano": Dapper / ADO.NET con IDbTransaction | "La mitad del trabajo" según ellos | Pestaña SQL |
| 3 | AWS→Azure: tabla de equivalencias y no-equivalencias | Pregunta 4, tu punto débil de cloud | Pestaña Cloud |
| 4 | Respuesta honesta y ensayada sobre tu inglés | Pregunta 5, la van a testear | Pestaña Guion |
| 5 | Native Federation: beneficios y dolores concretos, poder dibujarlo | Pregunta 2, tu fuerte — hay que lucirlo | Pestaña Angular |
Red de seguridad: si no pasa la prueba de SQL, sigues siendo candidato fuerte para el puesto de Front End Developer que NEWCO también tiene abierto. No es un fracaso — pero el objetivo es Full-Stack.
NEWCO LLC es la razón social; la marca es Vita Tienda de Cocó March (VitaTienda): tienda en línea de salud, bienestar y cuidado personal — suplementos, vitaminas, probióticos, kits, skincare, limpieza natural. Cara de marca: Dra. Cocó March (naturópata; las fórmulas se presentan como diseñadas por ella; claim de marketing "más de 1 millón de mujeres").
Mercado: hispano de EE. UU. + Latinoamérica. Operación bilingüe español/inglés, atención toda la semana. Es una empresa de e-commerce con equipo de desarrollo interno — no una empresa de tecnología. Footprint pequeño (12 seguidores en Computrabajo, 5 vacantes, sin reseñas de empleados).
store.dracocomarch.com/es/… → patrón de URL con id numérico = PrestaShop.tienda.dracocomarch.com/en-us/collections/… → Shopify (mercado /en-us).Lectura: lo más probable es que estén unificando PrestaShop + Shopify + Amazon detrás de un backend .NET propio (catálogo / inventario / pedidos como fuente de verdad), con Stripe para pago. "Integrar nuevos proveedores externos tomando ownership de toda la capa (back + front)" describe exactamente ese trabajo. Confírmalo preguntando (ver Guion → preguntas que haces).
| Área | Tecnología | Confianza |
|---|---|---|
| Frontend | Angular 20.3, TypeScript, RxJS, Angular Material, Bootstrap | Alta (JD + notas) |
| Frontend arquitectura | Microfrontends con Native Federation, federation.manifest.json | Alta (notas) |
| Testing frontend | Karma/Jasmine y Jest | Alta (notas) |
| Backend | .NET / ASP.NET, REST API | Alta (JD) |
| Acceso a datos | SQL Server (+ MySQL), escrito a mano — probablemente Dapper o ADO.NET, no EF pesado | Media — preguntar cuál |
| Cloud / CI | Azure, Azure DevOps (pipelines + boards) | Alta (JD) |
| Integraciones | Shopify Admin API, Amazon SP-API, Stripe | Alta (JD + notas) |
"Calidad, servicio cercano al cliente, experiencia de compra confiable, innovación en producto, excelencia en servicio." Traducción práctica: equipo chico, customer-obsessed, no romper producción (18 módulos vivos), alto impacto individual, ownership de punta a punta.
Sin datos públicos de LinkedIn (empresa no indexada). Espera 1–2 personas: quien redactó las notas (perfil técnico senior / tech lead, escribe "guion" y "pruebas técnicas" — lleva el proceso) y posiblemente alguien de producto u operación. Trata las notas como si vinieran del tech lead que decidirá.
"Escribe a mano un join de tres tablas con filtro por fecha y paginación, y explica la transacción." Practica hasta que salga sin pensar. Abajo: el modelo, luego cada concepto que te van a picar.
CREATE TABLE dbo.Customer (
CustomerId INT IDENTITY PRIMARY KEY,
FullName NVARCHAR(120) NOT NULL,
Email NVARCHAR(160) NOT NULL
);
CREATE TABLE dbo.Product (
ProductId INT IDENTITY PRIMARY KEY,
Sku VARCHAR(32) NOT NULL UNIQUE,
Name NVARCHAR(160) NOT NULL,
Stock INT NOT NULL CONSTRAINT CK_Product_Stock CHECK (Stock >= 0)
);
CREATE TABLE dbo.[Order] (
OrderId INT IDENTITY PRIMARY KEY,
CustomerId INT NOT NULL REFERENCES dbo.Customer(CustomerId),
OrderDate DATETIME2(0) NOT NULL,
Status VARCHAR(20) NOT NULL
);
CREATE TABLE dbo.OrderLine (
OrderLineId INT IDENTITY PRIMARY KEY,
OrderId INT NOT NULL REFERENCES dbo.[Order](OrderId),
ProductId INT NOT NULL REFERENCES dbo.Product(ProductId),
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL
);
-- índices que justifican el WHERE y el ORDER BY
CREATE INDEX IX_Order_Date ON dbo.[Order](OrderDate DESC, OrderId DESC) INCLUDE (CustomerId, Status);
CREATE INDEX IX_OrderLine_Order ON dbo.OrderLine(OrderId);
CREATE INDEX IX_OrderLine_Product ON dbo.OrderLine(ProductId);
-- Renglones de pedido de un rango de fechas, con cliente y producto, paginado.
SELECT
o.OrderId,
o.OrderDate,
c.FullName AS CustomerName,
p.Sku,
p.Name AS ProductName,
ol.Quantity,
ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Customer AS c ON c.CustomerId = o.CustomerId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @FromDate
AND o.OrderDate < @ToDateExclusive -- intervalo semiabierto: NO uses BETWEEN
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC -- orden determinista
OFFSET (@PageIndex * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY;
>= @From AND < @ToExclusive en vez de BETWEEN: si OrderDate tiene hora, BETWEEN '2026-01-01' AND '2026-01-31' pierde casi todo el día 31. Señal senior inmediata.CAST(o.OrderDate AS date) = …) — mata el uso del índice. Filtra contra la columna cruda.OFFSET/FETCH exige ORDER BY, y si el orden no es único la paginación repite o se salta filas entre páginas. Siempre termina con una columna única de desempate (PK).@PageIndex base 0.@FromDate), nunca concatenar strings: inyección SQL + reutilización de plan en caché.IX_Order_Date (OrderDate DESC, OrderId DESC) cubre filtro + orden; los FK de OrderLine evitan scans en los joins.LEFT JOIN dbo.Product. Decláralo como decisión, no por descuido.-- Opción A: dos queries en el mismo request (recomendado para páginas profundas)
SELECT COUNT(*)
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @FromDate AND o.OrderDate < @ToDateExclusive;
-- Opción B: en la misma query con ventana (cuesta un scan del set filtrado)
SELECT ..., COUNT(*) OVER() AS TotalRows
FROM ... WHERE ... ORDER BY ... OFFSET ... FETCH ...;
OFFSET grande es lento: el motor lee y descarta N filas. Para "cargar más" infinito o páginas profundas, keyset:
-- pasas el último (OrderDate, OrderId) de la página anterior
WHERE o.OrderDate >= @FromDate AND o.OrderDate < @ToDateExclusive
AND (o.OrderDate < @LastDate
OR (o.OrderDate = @LastDate AND o.OrderId < @LastOrderId))
ORDER BY o.OrderDate DESC, o.OrderId DESC
OFFSET 0 ROWS FETCH NEXT @PageSize ROWS ONLY;
Ventaja: coste constante por página, usa el índice directo. Desventaja: no puedes "saltar a la página 47".
Lo más probable: una escritura de varios pasos (crear pedido + renglones + descontar stock), que es justo el tipo de operación de un POS / recepción electrónica.
CREATE PROCEDURE dbo.CreateOrder
@CustomerId INT,
@Lines dbo.OrderLineTvp READONLY -- table-valued parameter
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- error en runtime => rollback automático, sin transacciones "doomed"
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.[Order] (CustomerId, OrderDate, Status)
VALUES (@CustomerId, SYSUTCDATETIME(), 'Pending');
DECLARE @OrderId INT = SCOPE_IDENTITY();
INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice)
SELECT @OrderId, l.ProductId, l.Quantity, l.UnitPrice
FROM @Lines AS l;
-- descuento de stock con bloqueo, en una sola sentencia (evita race read-then-write)
UPDATE p WITH (ROWLOCK)
SET p.Stock = p.Stock - l.Quantity
FROM dbo.Product AS p
JOIN @Lines AS l ON l.ProductId = p.ProductId;
-- el CHECK (Stock >= 0) ya protege; validación explícita para mensaje claro
IF EXISTS (SELECT 1 FROM dbo.Product p JOIN @Lines l ON l.ProductId = p.ProductId WHERE p.Stock < 0)
THROW 50001, 'Stock insuficiente para uno o más productos.', 1;
COMMIT TRANSACTION;
SELECT @OrderId AS OrderId;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION;
THROW; -- re-lanza; la capa .NET registra y traduce
END CATCH
END
| Concepto | Qué decir |
|---|---|
| ACID | Atomicidad (los 3 pasos se confirman juntos o ninguno), Consistencia (stock nunca negativo — CHECK + validación), Aislamiento (nivel configurable), Durabilidad (una vez COMMIT, sobrevive a caída). |
SET XACT_ABORT ON | Con esto, un error de runtime aborta y revierte toda la transacción automáticamente y evita dejar una transacción abierta/"doomed". Va al inicio del proc. Señal senior. |
XACT_STATE() / @@TRANCOUNT | Comprobar en el CATCH antes de hacer ROLLBACK. XACT_STATE() = -1 = transacción no confirmable, solo rollback. |
| Niveles de aislamiento | Default READ COMMITTED. Para "leer stock y luego actualizar" el riesgo es lost update; se resuelve haciendo el descuento en una sola UPDATE (arriba), o con hints UPDLOCK, HOLDLOCK, o SERIALIZABLE. Menciona READ COMMITTED SNAPSHOT (RCSI) para reducir bloqueos de lectura. |
| Deadlocks | Orden de acceso consistente a las tablas en todos los procs; reintento en error 1205 desde la capa de aplicación (Polly). |
| Concurrencia optimista | Columna rowversion comprobada en el WHERE del UPDATE; si afecta 0 filas, alguien más ya cambió el registro. En EF Core: atributo [Timestamp]. |
| Transacciones cortas | Nada de llamadas HTTP dentro de la transacción. El cobro a Stripe / la llamada a Shopify va fuera; se coordina con idempotency key + patrón outbox / saga. Esto conecta directo con su trabajo de integraciones — dilo tú. |
public async Task<IReadOnlyList<OrderLineRow>> GetLinesAsync(
DateTime fromDate, DateTime toExclusive, int pageIndex, int pageSize, CancellationToken ct)
{
const string sql = @"
SELECT o.OrderId, o.OrderDate, c.FullName AS CustomerName,
p.Sku, p.Name AS ProductName, ol.Quantity, ol.UnitPrice
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
JOIN dbo.Customer c ON c.CustomerId = o.CustomerId
JOIN dbo.Product p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @fromDate AND o.OrderDate < @toExclusive
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC
OFFSET (@pageIndex * @pageSize) ROWS FETCH NEXT @pageSize ROWS ONLY;";
await using var conn = new SqlConnection(_connString);
var cmd = new CommandDefinition(sql,
new { fromDate, toExclusive, pageIndex, pageSize }, cancellationToken: ct);
var rows = await conn.QueryAsync<OrderLineRow>(cmd);
return rows.AsList();
}
// transacción con Dapper
await using var conn = new SqlConnection(_connString);
await conn.OpenAsync(ct);
await using var tx = await conn.BeginTransactionAsync(ct);
try
{
var orderId = await conn.ExecuteScalarAsync<int>(insertOrderSql, param, tx);
await conn.ExecuteAsync(insertLinesSql, lines, tx);
await conn.ExecuteAsync(updateStockSql, lines, tx);
await tx.CommitAsync(ct);
}
catch { await tx.RollbackAsync(ct); throw; }
await using var conn = new SqlConnection(_connString);
await conn.OpenAsync(ct);
await using var tx = (SqlTransaction)await conn.BeginTransactionAsync(ct);
await using var cmd = new SqlCommand(sql, conn, tx);
cmd.Parameters.Add("@fromDate", SqlDbType.DateTime2).Value = fromDate;
cmd.Parameters.Add("@toExclusive", SqlDbType.DateTime2).Value = toExclusive;
cmd.Parameters.Add("@pageIndex", SqlDbType.Int).Value = pageIndex;
cmd.Parameters.Add("@pageSize", SqlDbType.Int).Value = pageSize;
await using var reader = await cmd.ExecuteReaderAsync(ct);
while (await reader.ReadAsync(ct)) { /* map por índice ordinal */ }
await tx.CommitAsync(ct);
Puntos: Parameters.Add con tipo explícito (evita conversiones implícitas que rompen el índice), using/await using en todo, CancellationToken, mapear por ordinal para velocidad.
// consulta LINQ equivalente
var q = _db.OrderLines
.Where(ol => ol.Order.OrderDate >= fromDate && ol.Order.OrderDate < toExclusive)
.OrderByDescending(ol => ol.Order.OrderDate).ThenByDescending(ol => ol.OrderLineId)
.Select(ol => new OrderLineRow { /* ... */ })
.Skip(pageIndex * pageSize).Take(pageSize);
// transacción explícita
await using var tx = await _db.Database.BeginTransactionAsync(ct);
// ... SaveChangesAsync varias veces ...
await tx.CommitAsync(ct);
// SQL crudo dentro de EF
_db.Database.SqlQuery<OrderLineRow>($"SELECT ... WHERE OrderDate >= {fromDate}");
Di que EF SaveChanges ya es transaccional por llamada; la transacción explícita es para agrupar varias llamadas o mezclar con SQL crudo. Concurrencia optimista con [Timestamp] rowversion.
GROUP BY + SUM + ORDER BY + TOP.LEFT JOIN … WHERE ol.OrderLineId IS NULL o NOT EXISTS.GROUP BY c.CustomerId, DATEFROMPARTS(YEAR(o.OrderDate), MONTH(o.OrderDate), 1).HAVING COUNT(*) > 3.CreateOrder completo, probando el rollback (mete un producto con stock 1 y pide 5).La versión "nunca toqué una base de datos": montar la BD, las tablas, meter datos, y entender el JOIN y la transacción línea por línea. Complementa la pestaña SQL (decide), no la reemplaza.
localhost (o .\SQLEXPRESS) → Authentication: Windows Authentication → Connect.GO no es SQL: es una señal para el editor que dice "manda lo de arriba y sigue". Se usa entre bloques.
Una base de datos es una carpeta que agrupa tablas relacionadas. Primero la creas, luego le dices al editor que trabaje dentro de ella.
CREATE DATABASE PruebaTienda;
GO
USE PruebaTienda; -- "de aquí en adelante, todo va dentro de PruebaTienda"
GO
Sin USE, tus tablas caerían en master (la de sistema) — eso está mal. Cada ventana nueva, empieza con USE PruebaTienda;.
Una tabla es una hoja de Excel: filas y columnas. Cada columna tiene un tipo:
| Tipo | Guarda | Ejemplo |
|---|---|---|
INT | enteros | 42 |
NVARCHAR(120) | texto hasta 120 chars (la N = acentos, ñ, emojis) | 'Ana López' |
VARCHAR(32) | texto simple hasta 32 | 'SKU-001' |
DECIMAL(10,2) | 10 dígitos, 2 decimales | 15.00 |
DATETIME2(0) | fecha y hora, sin fracciones de segundo | '2026-01-05 14:30:00' |
Y las reglas que le pones a una columna:
| Regla | Qué hace |
|---|---|
PRIMARY KEY | la columna que identifica cada fila de forma única. No se repite, no va vacía. |
IDENTITY | "numérame tú solo": 1, 2, 3… Nunca escribes ese valor a mano. |
NOT NULL | obligatoria, no puede quedar vacía. |
UNIQUE | no permite dos filas con el mismo valor. |
CHECK (condición) | regla de negocio (ej: stock nunca negativo). |
REFERENCES otraTabla(col) | llave foránea (FK): este valor tiene que existir en la otra tabla. |
El orden importa por las FK: no puedes crear una tabla que apunta a otra que aún no existe. Customer y Product primero; Order depende de Customer; OrderLine depende de las dos.
CREATE TABLE dbo.Customer (
CustomerId INT IDENTITY PRIMARY KEY, -- id autonumerado + PK
FullName NVARCHAR(120) NOT NULL,
Email NVARCHAR(160) NOT NULL
);
CREATE TABLE dbo.Product (
ProductId INT IDENTITY PRIMARY KEY,
Sku VARCHAR(32) NOT NULL UNIQUE,
Name NVARCHAR(160) NOT NULL,
Stock INT NOT NULL
CONSTRAINT CK_Product_Stock CHECK (Stock >= 0) -- nombre a la regla
);
CREATE TABLE dbo.[Order] ( -- corchetes: ORDER es palabra reservada
OrderId INT IDENTITY PRIMARY KEY,
CustomerId INT NOT NULL REFERENCES dbo.Customer(CustomerId), -- FK
OrderDate DATETIME2(0) NOT NULL,
Status VARCHAR(20) NOT NULL
);
CREATE TABLE dbo.OrderLine ( -- el detalle: qué productos y cuántos
OrderLineId INT IDENTITY PRIMARY KEY,
OrderId INT NOT NULL REFERENCES dbo.[Order](OrderId),
ProductId INT NOT NULL REFERENCES dbo.Product(ProductId),
Quantity INT NOT NULL,
UnitPrice DECIMAL(10,2) NOT NULL -- precio congelado al momento de la venta
);
-- índices: como el índice de un libro, para ir directo en vez de leer todo
CREATE INDEX IX_Order_Date ON dbo.[Order](OrderDate DESC, OrderId DESC) INCLUDE (CustomerId, Status);
CREATE INDEX IX_OrderLine_Order ON dbo.OrderLine(OrderId);
CREATE INDEX IX_OrderLine_Product ON dbo.OrderLine(ProductId);
GO
Un pedido (Order) tiene muchos renglones (OrderLine). Pedido #1 = "compra de Ana"; sus renglones = "2 Vitamina C" + "1 Probiótico".
INSERT INTO tabla (columnas) VALUES (valores). No pones el Id porque es IDENTITY (se autonumera).
INSERT INTO dbo.Customer (FullName, Email) VALUES
('Ana López','ana@mail.com'),('Beto Ruiz','beto@mail.com'),('Carmen Díaz','carmen@mail.com');
-- fíjate en los stocks bajos: sirven para probar fallos después
INSERT INTO dbo.Product (Sku, Name, Stock) VALUES
('SKU-001','Vitamina C',100),('SKU-002','Probiótico',50),('SKU-003','Omega 3',3),('SKU-004','Colágeno',0);
-- fechas repartidas en enero y febrero 2026 A PROPÓSITO
INSERT INTO dbo.[Order] (CustomerId, OrderDate, Status) VALUES
(1,'2026-01-05 10:00:00','Paid'),
(2,'2026-01-20 16:30:00','Paid'),
(1,'2026-02-03 09:15:00','Pending'), -- cae FUERA de un filtro de enero
(3,'2026-01-28 12:00:00','Paid');
INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice) VALUES
(1,1,2,15.00),(1,2,1,25.00),(2,3,1,30.00),(3,1,5,15.00),(4,2,3,25.00),(4,1,1,15.00);
GO
-- verifica que quedó todo
SELECT * FROM dbo.Customer;
SELECT * FROM dbo.Product;
SELECT * FROM dbo.[Order];
SELECT * FROM dbo.OrderLine;
Qué es un JOIN: cada tabla guarda un pedacito. El pedido sabe el id del cliente, no su nombre. Un JOIN "pega" las filas de varias tablas usando la columna que coincide.
SELECT
o.OrderId,
o.OrderDate,
c.FullName AS CustomerName, -- AS renombra la columna en el resultado
p.Sku,
p.Name AS ProductName,
ol.Quantity,
ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal -- columna calculada al vuelo
FROM dbo.OrderLine AS ol -- tabla base; "ol" es un apodo (alias)
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId -- pega Order donde el OrderId casa
INNER JOIN dbo.Customer AS c ON c.CustomerId = o.CustomerId -- pega el cliente
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId -- pega el producto
WHERE o.OrderDate >= '2026-01-01'
AND o.OrderDate < '2026-02-01' -- rango semiabierto (ver abajo)
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC -- orden determinista
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY; -- "salta 0, dame las siguientes 5"
INNER JOIN = solo filas que casan en ambos lados. LEFT JOIN = todas las de la izquierda aunque no haya pareja a la derecha.
BETWEEN. OrderDate tiene hora. BETWEEN '2026-01-01' AND '2026-01-31' significa <= '2026-01-31 00:00:00' y pierde todo el día 31. Correcto: >= inicio AND < inicio_del_siguiente_mes.WHERE CAST(o.OrderDate AS date) = … anula el índice. Filtra contra la columna cruda. La palabra elegante: predicado SARGable.OFFSET/FETCH obliga a ORDER BY. Si el orden no es único, la página 2 repite o se salta filas. Termina siempre con una columna única (PK) de desempate.-- fórmula: OFFSET = numeroDePagina * tamañoDePagina (la página empieza en 0)
... OFFSET 0 ROWS FETCH NEXT 2 ROWS ONLY; -- página 1
... OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY; -- página 2
... OFFSET 4 ROWS FETCH NEXT 2 ROWS ONLY; -- página 3
En código nunca pegas valores en el texto (inyección SQL + rendimiento). Usas variables con @:
DECLARE @FromDate DATETIME2(0) = '2026-01-01';
DECLARE @ToDateExclusive DATETIME2(0) = '2026-02-01';
DECLARE @PageIndex INT = 0;
DECLARE @PageSize INT = 5;
SELECT o.OrderId, o.OrderDate, c.FullName AS CustomerName,
p.Sku, p.Name AS ProductName, ol.Quantity, ol.UnitPrice,
ol.Quantity * ol.UnitPrice AS LineTotal
FROM dbo.OrderLine AS ol
INNER JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
INNER JOIN dbo.Customer AS c ON c.CustomerId = o.CustomerId
INNER JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @FromDate
AND o.OrderDate < @ToDateExclusive
ORDER BY o.OrderDate DESC, o.OrderId DESC, ol.OrderLineId DESC
OFFSET (@PageIndex * @PageSize) ROWS
FETCH NEXT @PageSize ROWS ONLY;
Qué es: un grupo de operaciones que pasan todas o ninguna. Analogía: transferencia bancaria = "restar de A" + "sumar a B". Si se cae la luz entre las dos, el dinero desaparece. La transacción lo evita.
Crear un pedido = 3 pasos: (1) insertar en Order, (2) insertar sus renglones, (3) descontar stock. Si el paso 3 falla y 1–2 ya corrieron → pedido fantasma. La transacción revierte todo.
BEGIN TRANSACTION; -- "anota todo en lápiz"
INSERT INTO dbo.[Order] (CustomerId, OrderDate, Status)
VALUES (1, SYSUTCDATETIME(), 'Pending');
DECLARE @OrderId INT = SCOPE_IDENTITY(); -- el id que se acaba de generar
INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice)
VALUES (@OrderId, 1, 2, 15.00);
UPDATE dbo.Product SET Stock = Stock - 2 WHERE ProductId = 1;
COMMIT TRANSACTION; -- "pásalo a tinta, ya es permanente"
-- (si escribes ROLLBACK TRANSACTION en vez de COMMIT: borra todo, como si nada)
CREATE PROCEDURE dbo.CreateSimpleOrder
@CustomerId INT, @ProductId INT, @Quantity INT, @UnitPrice DECIMAL(10,2)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- si algo revienta en runtime, revierte TODO solo
BEGIN TRY
BEGIN TRANSACTION;
INSERT INTO dbo.[Order] (CustomerId, OrderDate, Status)
VALUES (@CustomerId, SYSUTCDATETIME(), 'Pending');
DECLARE @OrderId INT = SCOPE_IDENTITY();
INSERT INTO dbo.OrderLine (OrderId, ProductId, Quantity, UnitPrice)
VALUES (@OrderId, @ProductId, @Quantity, @UnitPrice);
UPDATE dbo.Product SET Stock = Stock - @Quantity WHERE ProductId = @ProductId;
IF EXISTS (SELECT 1 FROM dbo.Product WHERE ProductId = @ProductId AND Stock < 0)
THROW 50001, 'Stock insuficiente.', 1; -- error propio (>= 50000 son tuyos)
COMMIT TRANSACTION;
SELECT @OrderId AS NuevoOrderId;
END TRY
BEGIN CATCH
IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; -- ¿hay transacción viva? revertirla
THROW; -- re-lanza el error para que .NET lo registre
END CATCH
END;
GO
| Línea | Explicación de tonto |
|---|---|
SET NOCOUNT ON | "no me mandes 'X rows affected' por cada insert". Limpia ruido, ahorra tráfico. |
SET XACT_ABORT ON | el seguro de vida: si una sentencia falla, revierte la transacción entera sola. Va al inicio. |
BEGIN TRY / BEGIN CATCH | "intenta esto; si falló, ejecuta esto otro". |
THROW 50001, '...', 1 | lanzar tu propio error a mano. |
XACT_STATE() <> 0 | "¿hay una transacción abierta ahora?" Si sí, ROLLBACK. Evita revertir algo que ya no existe. |
THROW; (sin argumentos) | re-lanza el error original hacia arriba, para que tu código C# lo loguee. |
-- Omega 3 (ProductId 3) tiene stock 3. Pide 5:
EXEC dbo.CreateSimpleOrder @CustomerId=1, @ProductId=3, @Quantity=5, @UnitPrice=30.00;
-- => error rojo. Ahora comprueba que NO quedó nada:
SELECT * FROM dbo.[Order] WHERE CustomerId = 1 ORDER BY OrderId DESC; -- sin pedido nuevo
SELECT Stock FROM dbo.Product WHERE ProductId = 3; -- sigue en 3, no en -2
-- Una que SÍ funciona:
EXEC dbo.CreateSimpleOrder @CustomerId=1, @ProductId=1, @Quantity=4, @UnitPrice=15.00;
SELECT Stock FROM dbo.Product WHERE ProductId = 1; -- bajó de 100 a 96
Orden de práctica (Día 1): 1) monta el script completo · 2) escribe el JOIN de memoria, sin copiar, hasta que salga solo · 3) mueve el OFFSET y observa · 4) explica en voz alta por qué no usas BETWEEN · 5) escribe el proc y prueba el rollback · 6) los 7 mini-drills de la pestaña SQL (decide).
| Promise | Observable | |
|---|---|---|
| Valores | uno solo | 0..∞ (un flujo en el tiempo) |
| Ejecución | eager: corre al crearse | lazy: corre al hacer subscribe() |
| Cancelable | no | sí: unsubscribe() aborta (p. ej. cancela el HTTP) |
| Operadores | .then()/.catch() | pipe(map, filter, switchMap, debounceTime, retry, combineLatest…) |
| Sync/async | siempre async (microtask) | puede emitir sync o async |
| Multi-suscriptor | comparte el único resultado | cold: cada subscribe reejecuta; hot/share(): multicast |
En Angular: HttpClient devuelve Observable → al desuscribir cancela la petición (ideal para typeahead con switchMap). Eventos del Router, form.valueChanges, interop con Signals (toSignal(), toObservable()). El pipe async se suscribe y desuscribe solo.
Fugas de memoria = suscripciones sin gestionar. Soluciones: pipe async, takeUntilDestroyed() (Angular 16+), DestroyRef.
| Operador | Comportamiento | Caso real en un POS / backoffice |
|---|---|---|
switchMap | cancela la anterior | búsqueda de producto: solo importa la última tecla |
mergeMap | todas en paralelo | disparar N validaciones independientes a la vez |
concatMap | cola, en orden | guardar renglones de un pedido en secuencia sin pisarse |
exhaustMap | ignora nuevas mientras trabaja | botón "confirmar venta": evita doble submit |
Extra: forkJoin (espera a que todas terminen — cargar datos de arranque), combineLatest (recombina al cambiar cualquiera — filtros), catchError + retry/retryWhen, shareReplay(1) (cachear la respuesta de un GET de catálogo).
Qué es: el concepto de Module Federation reconstruido sobre ESM nativo + import maps, para que funcione con el build moderno de Angular (esbuild/Vite). Webpack Module Federation dejó de encajar cuando Angular migró a esbuild; @angular-architects/native-federation es el reemplazo. La configuración de remotos vive en federation.manifest.json (el que tienes en tu repo).
shared.NG0203 (inject fuera de contexto). Alinear shared con singleton: true y strictVersion.federation.manifest.json por entorno.Prepárate a dibujarlo: shell (host) → lee federation.manifest.json → genera import map → carga remoteEntry de cada microfrontend bajo demanda → monta su ruta. Deps compartidas resueltas una vez.
computed, efectos con effect); RxJS para flujos asíncronos y coordinación de eventos. Interop con toSignal/toObservable. No migrar todo a Signals porque sí.OnPush + inmutabilidad + async pipe; ChangeDetectorRef. Con Signals, CD granular.imports: [...] en el componente.loadComponent / loadChildren con rutas; @defer blocks para diferir render.updateOn: 'blur'.Los mismos temas de la pestaña Angular, pero con el modelo mental masticado y la frase exacta que dices en la entrevista.
| Promise | Observable | |
|---|---|---|
| ¿Cuántos valores? | uno | 0, 1 o muchos (un flujo en el tiempo) |
| ¿Cuándo arranca? | eager: al crearla ya corrió | lazy: nada hasta .subscribe() |
| ¿Cancelable? | no | sí: unsubscribe(). En HTTP cancela la petición de verdad |
| ¿Transformar? | .then() / .catch() | .pipe(map, filter, switchMap, debounceTime…) |
| ¿Sync/async? | siempre async | puede emitir sync o async |
| Varios suscriptores | comparten el único resultado | cold: cada subscribe reejecuta; hot/share(): multicast |
// HttpClient SIEMPRE devuelve Observable
this.http.get<Product[]>('/api/catalog').subscribe(list => this.products = list);
switchMap es tan limpio.form.valueChanges, eventos del Router, paramMap → todos Observables.async se suscribe y desuscribe solo: <div *ngIf="product$ | async as p">{{ p.name }}</div>Fuga = te suscribes y nunca te desuscribes; el componente se destruye pero la suscripción sigue viva consumiendo memoria. Soluciones: 1) pipe async (la preferida) · 2) takeUntilDestroyed() (v16+) · 3) DestroyRef.
"Una promesa es un valor único, eager y no cancelable. Un Observable es un flujo perezoso: no corre hasta que te suscribes, puede emitir muchos valores y lo cancelas con unsubscribe. En Angular importa porque HttpClient devuelve Observables —desuscribirte cancela la petición real— y porque tengo operadores como switchMap o debounceTime para coordinar eventos asíncronos. El riesgo es olvidar desuscribirte; lo resuelvo con el pipe async o takeUntilDestroyed."
Tienes un Observable que emite valores (cada tecla). Por cada valor quieres lanzar otro Observable (una petición HTTP). Sin operador terminas con un Observable de Observables anidado e inútil. Aplanar = fundir el interno dentro del externo para tener un solo flujo. La pregunta clave: cuando llega un valor nuevo y el anterior no terminó, ¿qué hago?
| Operador | Con el anterior… | Caso real (POS / backoffice) |
|---|---|---|
switchMap | lo cancela | búsqueda de producto: solo importa la última tecla |
mergeMap | lo deja, todos en paralelo | N validaciones independientes a la vez |
concatMap | hace cola, en orden | guardar renglones de un pedido sin que se pisen |
exhaustMap | ignora los nuevos mientras trabaja | botón "Confirmar venta": anti doble-submit |
// buscador con switchMap
results$ = this.searchControl.valueChanges.pipe(
debounceTime(300), // espera 300ms sin teclear
distinctUntilChanged(), // si el texto no cambió, no busques
switchMap(term => this.api.searchProducts(term)) // cancela la búsqueda anterior
);
Extras que conviene nombrar: forkJoin (espera a que todas terminen — datos de arranque) · combineLatest (recombina al cambiar cualquiera — filtros) · shareReplay(1) (cachear un GET de catálogo) · catchError + retry.
"Se diferencian por lo que hacen cuando llega un valor nuevo antes de terminar el anterior. switchMap cancela —búsquedas—; mergeMap paraleliza; concatMap encola en orden —guardados secuenciales—; exhaustMap ignora los nuevos —botón de confirmar, anti doble submit."
En vez de una app Angular gigante, tienes varias apps pequeñas que se cargan al vuelo. El shell (host) es el marco: sidebar, header, router. Los remotos son los cuadros que cuelgas: "módulo de compras", "monitor de embarques". Cada uno en su repo y su deploy.
1. El shell arranca
2. Lee federation.manifest.json -> { "compras": "https://.../remoteEntry.json", ... }
3. Genera un "import map" (mapa de modulos ESM nativos del navegador)
4. El usuario navega a /compras
5. El shell descarga el remoteEntry de "compras" y monta su ruta
6. Angular, RxJS, Material se cargan UNA vez y se comparten
Por qué "Native": Module Federation vivía en Webpack. Angular migró su compilador a esbuild y Webpack MF dejó de encajar. @angular-architects/native-federation reconstruye la idea sobre ESM nativo + import maps.
NG0203. Se mitiga con shared: { singleton: true, strictVersion: true }."Es Module Federation reconstruido sobre ESM nativo para que funcione con el compilador esbuild de Angular. Me dio deploy independiente por microfrontend y dependencias compartidas. Lo que costó: alinear versiones —un remoto con otra versión de Angular te mete dos instancias y errores de DI— y que no hay comunicación entre microfrontends integrada, hay que montarla. El dev local también se complica porque necesitas todos los remotos arriba."
| Tema | Qué decir |
|---|---|
| Signals vs RxJS | Signals para estado síncrono de UI (computed, effect). RxJS para flujos asíncronos y eventos. Interop con toSignal/toObservable. No migrar todo "porque sí". |
| Change detection | Zone.js (default) vs zoneless (v18+). OnPush + inmutabilidad + pipe async. Con Signals, CD granular. |
| Standalone components | Default desde v17. Sin NgModule; dependencias en imports: [...] del componente. |
| Lazy loading | loadComponent / loadChildren en rutas. Bloques @defer para diferir render. |
| Formularios | Reactive Forms tipados (v14+), validadores async, updateOn: 'blur'. |
| Interceptores HTTP funcionales | (v15+) auth, retry, logging, y la idempotency-key de las integraciones. |
Eres el único del proceso que nombra las dos. Pregunta 3: "el último test de componente que escribiste y qué comprobaba." Necesitas una anécdota concreta y verificable. Rellena la plantilla con tu último test real.
@Output / que llamó al servicio con los args correctos / rama de error].TestBed con [imports standalone], mocks con [jasmine.createSpyObj / { provide: X, useValue }].fakeAsync+tick() / HttpTestingController / whenStable].describe('SaleButtonComponent', () => {
let fixture: ComponentFixture<SaleButtonComponent>;
let sales: jasmine.SpyObj<SalesService>;
beforeEach(async () => {
sales = jasmine.createSpyObj<SalesService>('SalesService', ['confirm']);
await TestBed.configureTestingModule({
imports: [SaleButtonComponent], // standalone
providers: [{ provide: SalesService, useValue: sales }],
}).compileComponents();
fixture = TestBed.createComponent(SaleButtonComponent);
});
it('deshabilita el botón mientras confirma', fakeAsync(() => {
sales.confirm.and.returnValue(timer(100).pipe(map(() => ({ ok: true }))));
fixture.componentInstance.total = 250;
fixture.detectChanges();
const btn = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
btn.click();
fixture.detectChanges();
expect(btn.disabled).toBeTrue(); // durante la operación
tick(100);
fixture.detectChanges();
expect(btn.disabled).toBeFalse();
expect(sales.confirm).toHaveBeenCalledOnceWith(250);
}));
});
TestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
const http = TestBed.inject(HttpTestingController);
service.getCatalog().subscribe(r => expect(r.length).toBe(2));
const req = http.expectOne('/api/catalog');
expect(req.request.method).toBe('GET');
req.flush([{ id: 1 }, { id: 2 }]);
http.verify();
const loader = TestbedHarnessEnvironment.loader(fixture);
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Confirmar' }));
await button.click();
const input = await loader.getHarness(MatInputHarness);
await input.setValue('ABC-123');
Ventaja: el test no depende del DOM interno de Material; sobrevive a upgrades.
| Karma/Jasmine | Jest | |
|---|---|---|
| Entorno | navegador real (Chrome headless) | jsdom (simulado en Node) |
| Velocidad | más lento (arranca navegador) | rápido, paralelo, watch mode ágil |
| Spies/mocks | jasmine.createSpy | jest.fn(), jest.mock() de módulos |
| Snapshots | no nativo | sí |
| Setup en Angular | viene por defecto | jest-preset-angular |
Por qué mantener ambos: Jest para el ciclo rápido de TDD y CI (miles de tests unitarios de lógica y componentes); Karma cuando necesitas fidelidad de navegador real — APIs del DOM que jsdom no implementa bien, layout, algunas cosas de Material/CDK, o pruebas que rozan lo e2e. Si te preguntan "¿los unificarías?": depende del coste de mantener dos configs vs. lo que se perdería; migrar a Jest todo lo unitario y dejar Karma/Playwright para lo que exige navegador es una postura defendible.
Tests de componente aislados: sin cambio. Lo difícil es la integración cruzando la frontera de federación — ahí conviene contract testing entre shell y remoto, o e2e (Playwright/Cypress) sobre el shell ensamblado.
Qué es un test, cómo se lee un spec de Angular, y cada palabra rara (TestBed, fixture, fakeAsync) traducida. Complementa la pestaña Testing.
Un test unitario = un trocito de código que verifica automáticamente que una pieza pequeña (un componente, un servicio) hace lo que debe. Se corre en cada commit; si algo se rompe, te enteras en segundos y no en producción.
Todo test tiene 3 fases (regla AAA):
describe('SaleButtonComponent', () => { // GRUPO de tests
let fixture: ComponentFixture<SaleButtonComponent>;
let sales: jasmine.SpyObj<SalesService>;
beforeEach(async () => { // ANTES DE CADA test (it)
sales = jasmine.createSpyObj<SalesService>('SalesService', ['confirm']); // servicio falso
await TestBed.configureTestingModule({ // arma un mini-Angular para el test
imports: [SaleButtonComponent], // el componente es standalone
providers: [{ provide: SalesService, useValue: sales }], // usa el falso, no el real
}).compileComponents();
fixture = TestBed.createComponent(SaleButtonComponent); // instancia + su DOM
});
it('deshabilita el boton mientras confirma', () => { /* AAA aqui */ });
});
| Palabra | Qué es |
|---|---|
describe('X', () => {}) | una carpeta de tests. Se puede anidar. |
it('hace Y', () => {}) | un test. Léelo como frase: "it deshabilita el botón mientras confirma". |
beforeEach | código que corre antes de cada it, para empezar limpio. |
TestBed | el "banco de pruebas": un Angular en miniatura configurado solo con lo necesario. |
fixture | envuelve la instancia del componente y su DOM. |
fixture.componentInstance | la clase (propiedades y métodos). |
fixture.nativeElement | el HTML renderizado (para querySelector, leer texto). |
fixture.detectChanges() | corre el change detection: aplica los cambios de datos al DOM. Sin esto el HTML no se actualiza. |
Un test unitario prueba una pieza. Si el componente usa SalesService (que llama a una API real), no quieres que el test llame a la API. Le pones un doble.
const sales = jasmine.createSpyObj<SalesService>('SalesService', ['confirm']);
sales.confirm.and.returnValue(of({ ok: true })); // "cuando te llamen, devuelve esto"
// ... despues de actuar ...
expect(sales.confirm).toHaveBeenCalledOnceWith(250); // "1 vez, con el argumento 250"
En Jest es lo mismo con jest.fn() y jest.spyOn().
| Assertion | Verifica |
|---|---|
expect(x).toBe(y) | igualdad estricta (===) — números, strings, misma referencia |
expect(x).toEqual(y) | igualdad profunda — objetos/arrays por contenido |
expect(x).toBeTrue() / toBeFalse() | booleanos exactos |
expect(fn).toHaveBeenCalledWith(a, b) | que un spy fue llamado con esos argumentos |
expect(() => fn()).toThrow() | que lanza un error |
it('deshabilita el boton mientras confirma', fakeAsync(() => {
// ARRANGE: el servicio falso devuelve un resultado que tarda 100ms
sales.confirm.and.returnValue(timer(100).pipe(map(() => ({ ok: true }))));
fixture.componentInstance.total = 250;
fixture.detectChanges(); // pinta el estado inicial
// ACT: el usuario da clic
const btn = fixture.nativeElement.querySelector('button') as HTMLButtonElement;
btn.click();
fixture.detectChanges();
// ASSERT 1: mientras corre, el boton esta deshabilitado
expect(btn.disabled).toBeTrue();
tick(100); // avanza el reloj falso 100ms de golpe
fixture.detectChanges();
// ASSERT 2 y 3
expect(btn.disabled).toBeFalse();
expect(sales.confirm).toHaveBeenCalledOnceWith(250);
}));
fakeAsync + tick() — controlar el tiempoCódigo async (un setTimeout, un timer, un debounceTime) no termina "ahora mismo". fakeAsync congela el reloj; tick(100) lo avanza 100ms de golpe y ejecuta lo que estuviera programado; flush() avanza hasta que no quede nada. Alternativa para promesas: async + await fixture.whenStable().
HttpTestingController — HTTP sin redTestBed.configureTestingModule({ imports: [HttpClientTestingModule] });
const http = TestBed.inject(HttpTestingController);
service.getCatalog().subscribe(r => expect(r.length).toBe(2)); // te suscribes
const req = http.expectOne('/api/catalog'); // verifica que se hizo UNA peticion a esa URL
expect(req.request.method).toBe('GET');
req.flush([{ id: 1 }, { id: 2 }]); // TU decides que "responde el servidor"
http.verify(); // no quedaron peticiones sin atender
No hay servidor: interceptas la petición, verificas cómo se hizo, y tú das la respuesta.
const loader = TestbedHarnessEnvironment.loader(fixture);
const button = await loader.getHarness(MatButtonHarness.with({ text: 'Confirmar' }));
await button.click();
const input = await loader.getHarness(MatInputHarness);
await input.setValue('ABC-123');
Ventaja: el test interactúa con la API pública del componente, no con su HTML interno. Sobrevive a los upgrades de Material.
| Karma / Jasmine | Jest | |
|---|---|---|
| Dónde corre | Chrome headless — navegador real | jsdom — navegador simulado en Node |
| Velocidad | más lento (arranca Chrome) | rápido, paralelo, --watch ágil |
| Spies/mocks | jasmine.createSpy | jest.fn(), jest.mock() de módulos |
| Snapshots | no nativo | sí |
| Setup en Angular | por defecto | jest-preset-angular |
Por qué ambos: Jest para el ciclo rápido de TDD y CI (miles de tests en segundos); Karma cuando necesitas fidelidad de navegador real (APIs del DOM que jsdom implementa mal, layout, ciertas cosas de Material/CDK).
"¿Los unificarías?" "Depende del costo de mantener dos configs vs. lo que se perdería. Postura defendible: migrar todo lo unitario a Jest y dejar Karma o Playwright solo para lo que exige navegador real. Pero primero mediría cuántos tests dependen de comportamiento real de navegador."
@Output / que llamó al servicio con los args correctos / rama de error.TestBed con imports standalone, mock con jasmine.createSpyObj.fakeAsync+tick() / HttpTestingController.Ejemplo hablado: "El último fue para un componente de confirmación de recepción. Cambié la lógica para deshabilitar el botón mientras la petición está en vuelo, para evitar doble submit. El test montaba el componente con TestBed, mockeaba el servicio con un spy que devolvía un observable con delay, hacía clic, verificaba btn.disabled === true, avanzaba el reloj con tick, y comprobaba que el servicio se llamó una sola vez. Usé fakeAsync."
Tests de componente aislados: sin cambio. Lo difícil es la integración cruzando la frontera de federación (shell ↔ remoto): contract testing entre shell y remoto, o e2e (Playwright/Cypress) sobre el shell ya ensamblado.
Drill (Día 3): 1) CounterComponent con botón que incrementa y emite un @Output cada 5 — test de valor inicial, de clic, y de emisión (spy del EventEmitter) · 2) CatalogService.getProducts() con HttpTestingController (expectOne, flush, verify) · 3) cualquier componente con setTimeout/debounceTime probado con fakeAsync+tick.
Pregunta 4: "tu nube es AWS Lambda, no Azure: ¿qué esperas que cambie?" Buena señal si nombras lo que NO equivale. No finjas experiencia en Azure que no tienes; demuestra que sabes traducir conceptos y dónde están las trampas.
| AWS | Azure | ¿Equivale limpio? |
|---|---|---|
| Lambda | Azure Functions | Parcial — Functions usa bindings declarativos (input/output por atributos); en Lambda escribes las llamadas al SDK a mano. Modelo isolated worker en .NET. |
| API Gateway | Azure API Management / HTTP trigger + Front Door | No — APIM es más pesado y caro; para casos simples basta el HTTP trigger de la Function. |
| Step Functions | Durable Functions / Logic Apps | No — Step Functions es una máquina de estados en JSON; Durable Functions es orquestación en código C#. Mentalidad distinta. |
| DynamoDB | Cosmos DB | No — Cosmos es multi-modelo, otro modelo de particionado y de costo (Request Units). |
| S3 | Blob Storage | Sí, cercano. |
| SQS / SNS | Storage Queues / Service Bus / Event Grid | No 1:1 — Service Bus ≈ SQS+SNS (colas + topics, sesiones, dead-letter); Storage Queues es lo simple; Event Grid es push de eventos. Son 3 cosas, no 1. |
| CloudWatch | Application Insights / Azure Monitor | App Insights es más rico para .NET (tracing distribuido casi gratis). |
| IAM roles | Managed Identity + RBAC (+ a veces App Registrations en Entra ID) | No — otro modelo mental de identidad y permisos. |
| SAM / CDK / Serverless Framework | Bicep / ARM / azd, y Azure DevOps Pipelines (ellos) | No — IaC y CI distintos; ellos ya usan Azure DevOps. |
"Mi experiencia de nube es AWS Lambda, así que lo que traigo es el modelo serverless: funciones stateless, cold starts, pensar en idempotencia y en límites de ejecución. Al pasar a Azure, lo que sí traslada casi directo es el enfoque event-driven, Blob ≈ S3, y App Insights me daría más de lo que tenía con CloudWatch. Lo que no equivale y tendría que aprender: Azure Functions usa bindings declarativos en vez de llamar al SDK a mano; Durable Functions no es Step Functions —es orquestación en código, no una máquina de estados JSON—; Cosmos no es DynamoDB en particionado ni en costeo; y la identidad pasa de IAM roles a Managed Identity + RBAC. Además, para el grueso de una API .NET probablemente no sería Functions sino App Service o Container Apps — ¿qué usan ustedes?"
Esa última pregunta te posiciona como alguien que piensa en arquitectura, no solo en servicios sueltos.
"Integrar nuevos proveedores externos tomando ownership de toda la capa." Ten estos patrones listos:
payment_intent.succeeded): verificar firma, responder 2xx rápido, procesar async, tolerar entregas duplicadas y fuera de orden.Stripe.net.Formato STAR: Situación · Tarea · Acción (tú, en primera persona, concreto) · Resultado (con número si puedes). 60–90 s cada una. Abajo están alineadas a los valores de NEWCO y a lo que van a dudar de ti.
Faltan tus insumos. Para dejar los borradores completos necesito 3–4 logros concretos tuyos (proyecto · tu rol · impacto medible · tecnología). Mientras, cada historia abajo tiene un esqueleto con lo que ya se sabe de tu paso por SEUS y marcado [RELLENAR] donde va tu dato real.
Por qué la hacen: el puesto es exactamente eso (modelo de datos → servicio → endpoint → componente).
S/T: En el POS/backoffice farmacéutico, el módulo de [recepción electrónica / compra directa / monitor de embarques — elige uno] necesitaba [RELLENAR: qué problema de negocio].
A: Diseñé el modelo de datos en SQL Server, escribí el acceso a datos y el servicio .NET, expuse el REST, y construí el componente Angular con Material. [RELLENAR: 1 decisión técnica difícil que tomaste].
R: Salió a producción y [RELLENAR: métrica — tiempo ahorrado, errores reducidos, volumen procesado]. Hoy es uno de los 18+ módulos vivos.
Por qué: corren 18 módulos en producción; "experiencia de compra confiable" es un valor.
Esqueleto: [RELLENAR: el bug] → detección (¿monitoreo? ¿reporte de usuario?) → causa raíz → fix inmediato → y la parte que importa: el test de regresión que agregaste (Karma o Jest) + [cambio de proceso: validación, alerta, revisión].
Por qué: el core del puesto es integrar Stripe/Shopify/SP-API.
Si tienes la historia: [RELLENAR: qué proveedor/servicio, qué falló — timeouts, rate limit, datos inconsistentes] → cómo lo hiciste resiliente (reintentos, idempotencia, conciliación).
Si no la tienes: dilo y pivota — "no he integrado un proveedor de pagos todavía, pero así es como lo abordaría" y explicas outbox + idempotency + webhooks (pestaña Cloud). Es honesto y demuestra criterio.
Native Federation es tu historia natural: [RELLENAR: por qué migraron a microfrontends, qué plazo tenías] → lo aprendiste, migraste [N] módulos, y los dolores que resolviste (version skew, comunicación entre MFEs). Resultado: deploy independiente por equipo.
Esqueleto: [RELLENAR: la decisión — p. ej. Karma vs Jest, estructura de un módulo, EF vs SQL a mano] → cómo expusiste tu posición con datos → qué acordaron → qué aprendiste de la otra postura. Clave: mostrar que cambias de opinión con evidencia y que priorizas el resultado del equipo.
Por qué: el JD pide "traducir requerimientos de negocio a specs técnicas".
Esqueleto: [RELLENAR: un requerimiento ambiguo de negocio] → preguntas que hiciste para acotarlo → cómo lo convertiste en spec → cómo validaste que era lo que querían antes de construir.
| Lo que dudan | Encuadre (di esto, con tu verdad) |
|---|---|
| Un solo empleador toda la carrera | "No cambié de empresa porque no dejé de crecer dentro: pasé de [rol inicial] a ser el mayor contribuidor del repo y a dueño de varios módulos críticos. Lo que sí cambió constantemente fue el stack debajo de mí — Angular 17→20, adoptar Native Federation, dos frameworks de test. Ahora busco ese cambio de entorno a propósito." |
| Pasante sin titular desde 2021 | Factual y hacia adelante: "[RELLENAR tu situación real: terminé el plan de estudios / me falta la titulación / estoy en proceso de X]. En estos años el trabajo real ha sido mi formación: 18 módulos en producción lo respaldan." No te disculpes, no te extiendas. |
| Adaptabilidad a un codebase nuevo | "Entré a un repo grande y en 4 años me volví su mayor contribuidor; sé leer código ajeno y moverme en un dominio complejo (retail farmacéutico con 18 módulos). Un codebase nuevo es la misma habilidad." |
"Soy desarrollador full-stack con 5 años en [SEUS], trabajando en un POS y backoffice para retail farmacéutico — hoy 18+ módulos en producción, cosas como compra directa, recepción electrónica y monitor de embarques.
Del lado del frontend es donde tengo más profundidad: Angular —ahora en 20—, RxJS, Angular Material, y migramos a microfrontends con Native Federation, donde fui el mayor contribuidor del repositorio con 86 commits. Testeo con Karma/Jasmine y con Jest.
Del lado del backend trabajo .NET con REST y SQL Server, con el acceso a datos escrito a mano. Mi experiencia de nube es AWS Lambda.
Busco un puesto full-stack de verdad, remoto, donde pueda tener ownership de features completas — y su stack es casi el mismo en el que ya me muevo, así que puedo contribuir rápido y a la vez profundizar más en la parte de .NET y datos."
Notas de entrega: di "5 años" (es lo que ya tienen en tu CV). Menciona SQL Server tú mismo, sin esperar a que lo saquen — le quita dramatismo a la pregunta 1. No escondas el inglés básico ni el AWS.
Tu nivel de inglés (te lo van a preguntar directo)
"Mi inglés es [básico / básico-intermedio — sé honesto] de lectura. Leo documentación técnica en inglés —de hecho la de Angular ya la consumo así— y me apoyo en traducción cuando el texto es denso. No lo hablo con fluidez todavía; lo estoy trabajando activamente [con práctica diaria — Anki, una app]. Para comunicación escrita asíncrona me defiendo; para una reunión en vivo en inglés todavía no estoy listo. Sé que SP-API, Stripe y Angular están documentados en inglés y con eso puedo."
No exageres — lo van a testear en la misma llamada. No te disculpes tres veces. Una respuesta, clara, con el plan de mejora.
Expectativa de salario
Ver pestaña Negociación. Resumen: pregunta su rango primero; si insisten, da un rango cuyo piso sea tu objetivo real, no tu mínimo.
Disponibilidad / cuándo puedes empezar
"Disponible de inmediato" ya lo tienen. Si sigues en SEUS, aclara tu tiempo de aviso real.
"No veo SQL en tu CV"
"Es cierto que no lo destaqué, y debí hacerlo: el acceso a datos en SQL Server es parte de mi día a día en el backend —consultas, joins, transacciones, todo escrito a mano—. Lo dejé implícito bajo '.NET / backend' y fue un error de redacción del CV, no de experiencia. De hecho por eso agradezco la prueba práctica."
Esa última pregunta resuelve el hueco del toolchain — si no te lo han dicho, te lo dicen ahí.
Sobre los datos de banda salarial: Levels.fyi y similares casi no tienen data de e-commerce pequeño en México, y NEWCO es una LLC de EE. UU. contratando remoto en MX — el esquema (nómina local, honorarios, contractor en USD) cambia el número por completo. Lo de abajo es estimación de mercado, no un dato duro. Confírmalo preguntando su rango.
| Escenario | Rango mensual aprox. (MXN) |
|---|---|
| Nómina local, empresa no-tech pequeña | 40,000 – 65,000 |
| Honorarios / contractor, pago influido por USD | 55,000 – 90,000+ |
| Tu objetivo declarado | hasta 80,000 |
Dado que es una LLC de EE. UU. y el puesto es "otro tipo de contrato" (probablemente honorarios/contractor), es razonable anclar en la parte alta.
Cuando pregunten "¿cuál es tu expectativa?"
"Antes de dar un número me ayudaría saber el esquema: ¿es nómina, honorarios, o contractor? ¿el pago es en pesos o en dólares? ¿qué prestaciones incluye? Con eso puedo darte una cifra que tenga sentido. ¿Cuál es el rango que tienen contemplado para la posición?"
Si insisten en que des número primero
"Con base en mi experiencia —5 años full-stack, Angular a nivel senior, microfrontends, y la parte de .NET y SQL Server que es justo lo que buscan— mi expectativa está en el rango de 65,000 a 85,000 MXN mensuales, ajustable según esquema y prestaciones."
El piso que dices (65k) es tu objetivo, no tu mínimo. Nunca ancles en 35k.
Contraoferta si ofrecen bajo
"Agradezco la oferta y el puesto me interesa mucho. El número está por debajo de lo que esperaba para este alcance —soy full-stack con ownership de la capa de integración y Angular senior—. ¿Hay margen para llegar a [tu cifra]? Si el fijo tiene tope, ¿podemos ver bono por desempeño, revisión a los 6 meses, o días de vacaciones?"
Si tienes menos días, colapsa 1–2 y 3, pero el Día 1 (SQL) y el Día 5 (entorno + ensayo) son intocables.
Las pestañas SQL · paso a paso, Angular · paso a paso y Testing · paso a paso tienen la versión explicada desde cero de los Días 1–3.
COUNT(*) OVER()).CreateOrder completo. Prueba el rollback. Explica en voz alta: ACID, XACT_ABORT, XACT_STATE, niveles de aislamiento, deadlocks, concurrencia optimista.IDbTransaction.shareReplay.forkJoin, combineLatest, catchError+retry, takeUntilDestroyed.fakeAsync/tick) y 1 test de servicio (HttpTestingController).MatButtonHarness / MatInputHarness.SELECT.dotnet --info funciona.Microsoft.Data.SqlClient ya referenciados. No quieres pelear con dotnet new durante la prueba.ng version funciona. Un proyecto Angular que compile y corra ng test verde.Ownership de feature vertical = como ya trabajo · stack 90% igual al mío · producto real, equipo chico, impacto directo · quiero profundizar en .NET + integraciones · remoto.
>= @From AND < @ToExclusive — NUNCA BETWEEN. Nada de funciones sobre la columna.ORDER BY determinista + columna única de desempate, luego OFFSET/FETCH. Keyset para páginas profundas.(OrderDate DESC, OrderId DESC).SET XACT_ABORT ON + TRY/CATCH + IF XACT_STATE() <> 0 ROLLBACK + THROW.UPDLOCK · deadlock → orden consistente + retry 1205 · concurrencia optimista con rowversion.conn.QueryAsync<T>(sql, params), transacción con BeginTransactionAsync y pasar tx.Observable: flujo (0..∞) · lazy · cancelable · operadores · cold reejecuta. Promise: uno · eager · no cancelable · .then.
4 aplanados: switch=cancela (búsqueda) · merge=paralelo · concat=cola ordenada · exhaust=ignora mientras trabaja (submit).
Module Federation sobre ESM + import maps para esbuild. Aporta: deploy independiente, integración runtime, deps compartidas. Cuesta: version skew (dos Angular → NG0203), comunicación entre MFEs sin solución nativa, routing, dev local, debugging opaco.
Step Functions ≠ Durable Functions (JSON vs código C#) · DynamoDB ≠ Cosmos (particionado, RU) · IAM roles ≠ Managed Identity + RBAC · API Gateway ≠ APIM · Lambda→Functions con bindings declarativos · una API .NET grande probablemente va en App Service / Container Apps, no Functions.
"Básico de lectura, consumo docs técnicos en inglés, lo trabajo activamente a diario, no conversacional aún. Para SP-API/Stripe/Angular me alcanza."
Un empleador = crecí dentro sin dejar de hacerlo (rol inicial → mayor contribuidor + dueño de módulos). El stack cambió constantemente aunque la empresa no. Titulación: [tu frase real], factual y hacia adelante.
Pregunta su rango + esquema (nómina/honorarios) + moneda PRIMERO. Si insisten: 65,000–85,000 MXN. Nunca menciones 35k.
SQL Server + .NET SDK + Angular CLI ~20 montados y probados · solución scratch pre-creada · BD de práctica sembrada · screen share testeado · correo previo confirmando el entorno.