Cada empresa a la que postulaste pide alguna combinación de Angular + .NET + SQL Server + Git/Azure DevOps. Esto cubre lo que preguntan, en los dos formatos: lo que dices (entrevista hablada) y lo que escribes (prueba en vivo o take-home).
| Tipo | Qué evalúan | Pestañas clave |
|---|---|---|
| Screening con reclutador | Encaje de perfil, salario, disponibilidad, inglés | Guion hablado · Inglés |
| Técnica hablada (con lead o senior) | Profundidad real: por qué, trade-offs, experiencia concreta | Angular hablado · .NET hablado · Arquitectura |
| Prueba en vivo / pair (pantalla compartida) | Que resuelvas tecleando y pensando en voz alta | Angular escrito · .NET escrito · SQL · Comandos |
| Take-home (te mandan un repo o enunciado) | Estructura, pruebas, README, git limpio | .NET escrito · Angular escrito · Comandos · Arquitectura |
De un relato de primera mano del proceso de OpenAI — y aplica igual a Meta, Uber, SpaceX y Netflix, porque todas usan la misma plataforma (CodeSignal).
No sobre-prepares el caso complejo y descuides el simple.
El patrón que más se repite en estas vacantes no es un JOIN de cinco tablas: es una consulta sencilla bien argumentada (a veces sobre una sola tabla) + el método de servicio que la ejecuta + el endpoint que la expone, muchas veces devolviendo un envelope Result<T> (processCorrect, typeError, data). Y la parte de Angular suele ser teclear un componente completo, no solo hablarlo. Todo eso está resuelto en las pestañas escritas.
Esta biblia se lee. Para practicar, está El Dojo Técnico — genera ejercicios de SQL, Angular, .NET, lógica y big tech, te deja escribir la respuesta, la evalúa contra una rúbrica y después te muestra cómo se hacía y por qué.
| Requisito típico | Tú | Cómo lo manejas |
|---|---|---|
| Angular 16–20, RxJS, Material | ✅ Fuerte | 5 años, migración a microfrontends, mayor contribuidor del repo |
| .NET Core Web API, C#, REST | ✅ Sólido | APIs serverless en AWS Lambda, servicio creado de cero, patrón 4 capas |
| SQL Server, consultas, JOINs | 🟡 Medio | Lo usas vía ORM/servicios; practica consultas a mano — el punto débil del CV |
| Azure / EF Core | 🟡 Parcial | Tu nube es AWS; EF lo conoces pero usas más ADO/Dapper. Di la verdad y traza el paralelo |
| Inglés avanzado / C1 | ❌ Básico | Filtro duro. Si es "deseable", auto-intro de 40 s preparada (pestaña Inglés) |
| Título expedido | ❌ Pasante | "Pasante de ITICS, ITTLA. La titulación está en trámite." No mentir |
| Liderazgo / Tech Lead | 🟡 De facto | Lideraste la migración y estandarizaste errores sin el título de lead — hay historia STAR |
switchMap, debounceTime). Pestaña Angular escrito.Result<T> — el trío que piden una y otra vez. Pestaña .NET escrito.Feedback directo de tu ex jefe. Esto pesa tanto como el código: un senior se distingue por cómo explica, no solo por resolver.
Cero "creo", "siento", "pienso", "me parece", "más o menos", "no sé si...". Delatan falta de experiencia. Si no sabes algo, dilo con firmeza: "Eso no lo he usado en producción. Lo que sí conozco es X, y el concepto equivalente sería Y." Decir "no sé" con seguridad vale más que inventar con duda.
Cada respuesta va situada: dónde lo usaste, qué problema resolvía, qué decidiste. No "sí, sé usar interceptores" sino "En Seus estandaricé el manejo de errores del front con un interceptor HTTP: antes cada módulo capturaba a su manera; lo unifiqué en un flujo único basado en promesas con un guard de sesión y roles."
El término exacto, no el rodeo. "Change detection", no "cuando Angular revisa los cambios". "Idempotente", "covering index", "optimistic concurrency", "hot vs cold observable", "tree-shaking". Un término preciso = años de experiencia.
| Flojo | Con molde |
|---|---|
"Sí, más o menos sé de switchMap, creo que sirve para cancelar cosas." |
"switchMap cancela el observable interno anterior cuando llega uno nuevo (afirmación). Lo usé en el buscador de productos del POS de Seus (contexto): sin él tenías una race condition — una respuesta lenta pisaba a una rápida (término). Para peticiones que no se deben perder, como guardar, ahí sí uso concatMap o exhaustMap (matiz)." |
Banco de preguntas por tema, con respuesta modelo anclada a tu experiencia en Seus. Aparecen en Hitss, Pyramid, INTERWARE, PROEXCELENCIA, Coppel, BC Tecnología, TESYS21 y prácticamente cualquier vacante de Angular del mercado.
¿Diferencia entre Observable y Promesa? ¿Cuándo usas cada uno?
La Promesa emite un solo valor, es eager (arranca al crearse) y no se puede cancelar. El Observable emite 0..N valores en el tiempo, es lazy (no hace nada hasta que te suscribes) y cancelable vía unsubscribe.
Contexto Seus: "El HTTP de Angular devuelve Observable, así que en la app casi todo es RxJS. Pero para un flujo transaccional de un solo disparo —como MasterGuardarPedido, guardar un pedido— lo modelé con promesas: no hay stream, es una operación puntual, y el patrón de 4 capas del proyecto se apoya en promesas para la orquestación."
Subject/fromEvent es hot); los 4 flatteners (abajo); toSignal/toObservable para el interop con signals; fugas si no te desuscribes.Explica switchMap, mergeMap, concatMap, exhaustMap.
| Operador | Qué hace con el interno | Caso de uso |
|---|---|---|
switchMap | Cancela el anterior si llega uno nuevo | Buscador / autocomplete / navegación — solo importa el último |
mergeMap | Corre todos en paralelo | Peticiones independientes, sin orden — p. ej. subir varios archivos |
concatMap | Uno tras otro, en orden, encola | Operaciones que deben ir en secuencia — guardados dependientes |
exhaustMap | Ignora nuevos mientras uno esté activo | Botón "Guardar" / login — evita el doble submit |
mergeMap: te llegan resultados desordenados. Para búsqueda es switchMap. Para un submit, exhaustMap."¿Cómo evitas fugas de memoria con suscripciones?
takeUntilDestroyed() (Angular 16+) — se engancha al DestroyRef, es lo más limpio hoy.AsyncPipe en el template — se desuscribe solo. La regla: si puedes resolverlo con | async, hazlo.toSignal() — maneja la desuscripción por ti.takeUntil(this.destroy$) con un Subject en ngOnDestroy.Contexto: "En el refactor de manejo de errores del front reemplacé suscripciones sueltas dispersas por un flujo único; ahí aproveché para cerrar fugas que venían de años."
¿Qué son los signals y qué cambian?
Un signal es un contenedor reactivo de un valor: count = signal(0), lees con count(), escribes con set/update. computed() deriva y memoiza; effect() reacciona a cambios para side-effects.
Lo que cambian: change detection granular. Angular sabe exactamente qué vista depende de qué signal y actualiza solo eso, sin recorrer el árbol. Es el camino a zoneless (sin Zone.js).
effect() suele ser un olor a código — si lo estás usando para setear otro estado, casi siempre quieres computed. effect es para logging, analytics, sincronizar con algo externo al framework."¿Cómo funciona change detection y qué es OnPush?
Por defecto (Zone.js) Angular parcha las APIs async del browser; cuando algo asíncrono termina, corre change detection sobre todo el árbol comparando bindings.
ChangeDetectionStrategy.OnPush: ese componente solo se revisa si (1) cambia una @Input por referencia, (2) dispara un evento del propio componente, (3) un observable ligado con async emite, o (4) lo marcas a mano con markForCheck(). Menos revisiones = más rendimiento. Con signals, el componente se marca solo cuando cambia un signal que usa.
Cuéntame de la migración a microfrontends con Native Federation.
"El producto era un monolito Angular de 18+ módulos. Migré los de backoffice —compra directa, recepción electrónica, monitor de embarques, pedido manual— a Angular 20 con Native Federation: una arquitectura host + remotos, cada módulo desplegable por separado, con una librería Angular compartida para componentes y servicios comunes. Fui el mayor contribuidor de ese repo, 86 commits."
¿Cuándo NO usarías microfrontends?
¿Cómo organizas los servicios y el consumo de APIs?
"Diseñé un patrón de integración en 4 capas para consumir los microservicios .NET (lealtad, cotizador de venta, clientes, facturación CFDI): Interfaz (contrato) → Service (HttpClient, mapeo) → Assembly (arma el request / desarma la respuesta) → Promise (orquesta y expone al componente). Aísla al componente del transporte y de la forma del backend."
¿Reactive Forms o Template-driven? ¿Cómo haces un validador custom?
Reactive para cualquier cosa no trivial: el modelo vive en el TS, es testeable, tipado, y compones validadores. Un validador custom es una función (control: AbstractControl): ValidationErrors | null; si es asíncrono devuelve Observable<ValidationErrors|null>. Validación cruzada entre campos → validador a nivel del FormGroup.
export function rfcValido(): ValidatorFn {
return (c: AbstractControl): ValidationErrors | null =>
/^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$/.test(c.value ?? '')
? null : { rfc: true };
}
¿Qué es la inyección de dependencias en Angular? ¿providedIn: 'root'?
Angular tiene un inyector jerárquico. @Injectable({ providedIn: 'root' }) registra el servicio en el inyector raíz como singleton y además es tree-shakable (si nadie lo inyecta, no entra al bundle). Puedes acotar el scope proveyéndolo a nivel de componente o ruta. inject() es la forma moderna, funciona fuera del constructor (en funciones, guards, computed).
InjectionToken para inyectar cosas que no son clases: config, un valor, una interfaz (como en el buscador sin HttpClient, pestaña Angular escrito).
Lazy loading y @defer.
Lazy loading de rutas: loadComponent / loadChildren — el código de esa ruta se baja cuando navegas. @defer (v17+) hace lazy a nivel de bloque del template, con triggers (on viewport, on idle, on interaction) y bloques @placeholder/@loading/@error. Sirve para bajar cosas pesadas below-the-fold sin partir en rutas.
¿Cómo pruebas un servicio que hace HTTP? ¿Y un componente?
Servicio HTTP: HttpClientTestingModule + HttpTestingController — expectOne, flush, y verify() en el afterEach. Nada pega a la red.
Componente: TestBed, ComponentFixture, fixture.detectChanges(). Para código async, fakeAsync + tick() (control determinista del tiempo) o waitForAsync. Para interacción de Material, los component harnesses (MatButtonHarness, etc.) en vez de escarbar el DOM.
Contexto: "El proyecto usa Karma/Jasmine y también Jest. El último spec que escribí fue del MasterGuardarPedidoService, cubriendo las 3 ramas: éxito, error de negocio y error de red."
TypeScript: tipos vs interfaces (interface se puede reabrir/extender, type compone uniones), generics, unknown vs any, keyof, utility types (Partial, Pick, Omit, Record), strict mode.
Standalone: es el default desde v17+; se acabaron los NgModules. Importas lo que usa el componente en su imports.
Control flow: @if / @for / @switch reemplazan *ngIf/*ngFor; @for exige track.
Interceptores: funcionales (HttpInterceptorFn) desde v15; auth token, manejo de error centralizado, loading, retry.
Seguridad front: Angular escapa por defecto (previene XSS); DomSanitizer solo cuando de verdad hace falta; nunca bypassSecurityTrust* con input de usuario.
No usas NgRx: "El estado del proyecto se maneja con servicios + RxJS/signals. NgRx lo conozco de concepto (store, actions, reducers, effects, selectors) pero para el tamaño de nuestros módulos hubiera sido overhead."
Rendimiento: OnPush, trackBy/track, lazy loading, @defer, evitar funciones en el template, NgOptimizedImage, virtual scroll del CDK.
Cada uno: enunciado, tiempo objetivo, pistas y solución comentada colapsadas, y qué decir en voz alta. Practica tecleando, no leyendo.
Componente ProductSearchComponent: input de búsqueda, lista de resultados, estados de carga / vacío / error. La fuente de datos no debe ser HttpClient directamente. Debe hacer debounce y cancelar búsquedas viejas.
ProductSearchService, e inyectarla con un token. Así el componente no conoce el transporte y es testeable sin HttpTestingController. Una implementación puede ser datos en memoria, otra fetch, otra HttpClient — el componente no cambia."InjectionToken<ProductSearchService> con el método search(term): Observable<Product[]>.FormControl + valueChanges → debounceTime(300) → distinctUntilChanged() → switchMap.catchError que devuelve of([]) — un error no debe completar el stream.finalize para apagar el loading (también corre al cancelar).loading, error, results. Desuscripción con takeUntilDestroyed().export interface Product { sku: string; nombre: string; precio: number; }
export interface ProductSearchService { search(term: string): Observable<Product[]>; }
export const PRODUCT_SEARCH = new InjectionToken<ProductSearchService>('PRODUCT_SEARCH');
@Injectable()
export class InMemoryProductSearch implements ProductSearchService {
private readonly catalogo: Product[] = [
{ sku: 'VT-001', nombre: 'Colágeno Hidrolizado', precio: 599 },
{ sku: 'VT-002', nombre: 'Omega 3', precio: 449 },
];
search(term: string): Observable<Product[]> {
const q = term.trim().toLowerCase();
const r = q ? this.catalogo.filter(p =>
p.nombre.toLowerCase().includes(q) || p.sku.toLowerCase().includes(q)) : [];
return of(r).pipe(delay(150));
}
}
@Component({
selector: 'app-product-search',
standalone: true,
imports: [ReactiveFormsModule, CurrencyPipe],
template: `
<input type="search" [formControl]="term" placeholder="Buscar producto o SKU…" />
@if (loading()) { <p>Buscando…</p> }
@else if (error()) { <p class="error">{{ error() }}</p> }
@else if (term.value && results().length === 0) { <p>Sin resultados</p> }
@else {
<ul>@for (p of results(); track p.sku) {
<li>{{ p.sku }} — {{ p.nombre }} ({{ p.precio | currency:'MXN' }})</li>
}</ul>
}`,
})
export class ProductSearchComponent {
private readonly api = inject(PRODUCT_SEARCH);
readonly term = new FormControl('', { nonNullable: true });
readonly loading = signal(false);
readonly error = signal<string | null>(null);
readonly results = signal<Product[]>([]);
constructor() {
this.term.valueChanges.pipe(
debounceTime(300),
map(t => t.trim()),
distinctUntilChanged(),
tap(() => this.error.set(null)),
switchMap(t => {
if (!t) { this.results.set([]); return EMPTY; }
this.loading.set(true);
return this.api.search(t).pipe(
catchError(() => { this.error.set('Error al buscar'); return of([] as Product[]); }),
finalize(() => this.loading.set(false)),
);
}),
takeUntilDestroyed(),
).subscribe(list => this.results.set(list));
}
}
// Provider: { provide: PRODUCT_SEARCH, useClass: InMemoryProductSearch }
Lista de pedidos con paginación del lado servidor (page, pageSize), ordenar por columna, y mostrar el total. El servicio devuelve { data: T[]; totalCount: number }.
query = signal({ page: 1, pageSize: 10, sortBy: 'fecha', sortDir: 'desc' }).toObservable(query) → switchMap al servicio; o rxResource({ request: () => query(), loader: ... }).Math.ceil(totalCount / pageSize).type Query = { page: number; pageSize: number; sortBy: string; sortDir: 'asc'|'desc' };
@Component({ /* ... */ })
export class OrdersTableComponent {
private readonly api = inject(ORDERS_API);
readonly query = signal<Query>({ page: 1, pageSize: 10, sortBy: 'fecha', sortDir: 'desc' });
private readonly page$ = toObservable(this.query).pipe(
switchMap(q => this.api.list(q).pipe(startWith(null))), // null = loading
);
readonly result = toSignal(this.page$, { initialValue: null });
readonly totalPages = computed(() => {
const r = this.result(); const q = this.query();
return r ? Math.ceil(r.totalCount / q.pageSize) : 0;
});
sortBy(col: string) {
this.query.update(q => ({
...q, page: 1, sortBy: col,
sortDir: q.sortBy === col && q.sortDir === 'asc' ? 'desc' : 'asc',
}));
}
goTo(page: number) { this.query.update(q => ({ ...q, page })); }
}
switchMap para cancelar la anterior. Cambiar el orden vuelve a página 1 porque la posición ya no tiene sentido."Alta de producto: sku (requerido, patrón), precio (> 0), precioOferta (opcional, debe ser < precio). Deshabilitar submit si inválido, mostrar errores por campo.
const form = this.fb.nonNullable.group({
sku: ['', [Validators.required, Validators.pattern(/^[A-Z]{2}-\d{3,}$/)]],
precio: [0, [Validators.required, Validators.min(0.01)]],
precioOferta: [null as number | null],
}, { validators: ofertaMenorQuePrecio });
function ofertaMenorQuePrecio(g: AbstractControl): ValidationErrors | null {
const precio = g.get('precio')?.value;
const oferta = g.get('precioOferta')?.value;
return oferta != null && oferta >= precio ? { ofertaInvalida: true } : null;
}
Template: @if (form.controls.sku.touched && form.controls.sku.errors?.['required'])… y [disabled]="form.invalid" en el botón.
FormGroup, no en el control — un control no ve a sus hermanos. Marco los errores solo cuando el campo fue tocado para no gritarle al usuario mientras escribe."Result<T> 15 minexport const apiInterceptor: HttpInterceptorFn = (req, next) => {
const auth = inject(AuthService);
const token = auth.token();
const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req;
return next(authReq).pipe(
map(event => {
if (event instanceof HttpResponse && event.body?.processCorrect === false) {
throw new HttpErrorResponse({ error: event.body, status: 422, url: req.url });
}
return event;
}),
catchError((err: HttpErrorResponse) => {
if (err.status === 401) auth.logout();
const msg = err.error?.messageError ?? 'Error de red';
inject(ToastService).error(msg);
return throwError(() => err);
}),
);
};
Result<T> lo desempaqueto aquí, en un solo lugar: si processCorrect es false lo convierto en error para que los componentes solo manejen el camino feliz. El 401 dispara logout global."export const rolGuard = (rol: string): CanActivateFn => () => {
const auth = inject(AuthService);
const router = inject(Router);
return auth.tieneRol(rol) ? true : router.createUrlTree(['/sin-acceso']);
};
// spec
it('redirige si no tiene el rol', () => {
TestBed.configureTestingModule({
providers: [
{ provide: AuthService, useValue: { tieneRol: () => false } },
{ provide: Router, useValue: { createUrlTree: jasmine.createSpy().and.returnValue('URLTREE') } },
],
});
const result = TestBed.runInInjectionContext(() => rolGuard('admin')(null!, null!));
expect(result).toBe('URLTREE' as any);
});
describe('DisponibilidadService', () => {
let svc: DisponibilidadService;
let http: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DisponibilidadService],
});
svc = TestBed.inject(DisponibilidadService);
http = TestBed.inject(HttpTestingController);
});
afterEach(() => http.verify());
it('devuelve la lista en éxito', () => {
let out: any;
svc.porSku('VT-001').subscribe(r => (out = r));
http.expectOne('/api/productos/VT-001/disponibilidad')
.flush([{ almacen: 'CDMX', cantidad: 40 }]);
expect(out.length).toBe(1);
});
it('propaga el error de red', () => {
let err: any;
svc.porSku('X').subscribe({ error: e => (err = e) });
http.expectOne(() => true).flush('boom', { status: 500, statusText: 'Server Error' });
expect(err.status).toBe(500);
});
it('trata processCorrect=false como error', () => {
let err: any;
svc.porSku('X').subscribe({ error: e => (err = e) });
http.expectOne(() => true).flush({ processCorrect: false, messageError: 'no existe' });
expect(err.message).toContain('no existe');
});
});
Cae en Wayssen, PROEXCELENCIA, peoplemx, Softtek, Hitss, ProceTI, TESYS21, Endava. Softtek es mantenimiento legacy (ASP.NET, ADO.NET, ODBC) — no todo es .NET moderno.
¿async/await? ¿Qué pasa por debajo?
El compilador convierte el método en una máquina de estados. await en una tarea no completada devuelve el control al llamador; cuando la tarea termina, la continuación se reanuda (por defecto en el contexto capturado). No crea hilos — libera el hilo mientras hay I/O pendiente. En una Web API eso significa más throughput: el hilo atiende otra request en vez de bloquearse esperando la BD.
Task vs ValueTask; ConfigureAwait(false) en librerías (en ASP.NET Core ya no hay SynchronizationContext, así que importa menos); nunca .Result/.Wait() (deadlock); CancellationToken propagado hasta el SqlCommand.IEnumerable<T> vs IQueryable<T>.
IEnumerable ejecuta en memoria (LINQ to Objects): si filtras después de traer, ya trajiste todo. IQueryable construye un árbol de expresión que el proveedor (EF) traduce a SQL — el filtro llega a la BD. El bug típico: hacer .ToList() antes de .Where() y traer la tabla entera.
¿record vs class? ¿Cuándo cuál?
record: igualdad por valor, inmutable por defecto, with para copias, ToString útil. Ideal para DTOs, requests/responses, value objects. class para entidades con identidad y comportamiento mutable. En APIs uso record para los DTO casi siempre.
Ejecución diferida en LINQ.
Los operadores como Where/Select no ejecutan hasta que enumeras (foreach, ToList, Count, First). Consecuencias: si iteras dos veces, la consulta corre dos veces; si la fuente cambió, el resultado cambia; con EF, materializa con ToListAsync antes de cerrar el contexto.
Ciclos de vida de DI: Singleton, Scoped, Transient.
| Vida | Instancia | Uso |
|---|---|---|
| Singleton | Una para toda la app | Config, cache, cliente HTTP factory, cosas sin estado por request |
| Scoped | Una por request HTTP | DbContext, unit of work, repos, servicios de negocio |
| Transient | Una por cada resolución | Servicios ligeros y sin estado |
DbContext eso truena porque no es thread-safe. El contenedor de .NET lo detecta en Development.El pipeline de middleware. ¿Orden importa?
Cada middleware puede actuar antes y después del next() — es una cebolla. El orden es crítico: UseRouting → UseAuthentication → UseAuthorization → endpoints. UseExceptionHandler va primero para envolver todo. CORS antes de auth. Un middleware custom típico: logging de request, correlación de IDs, o el manejo global de excepciones que traduce a Result/ProblemDetails.
Minimal APIs vs Controllers.
Minimal APIs: menos ceremonia, rápidas de escribir, buenas para servicios chicos y microservicios. Controllers: convenciones, filtros por atributo, model binding rico, mejor para APIs grandes con muchos endpoints y lógica transversal. Ambas usan el mismo binding y DI. "En Seus las Lambdas eran controllers con arquitectura Controllers–Business–DTO."
Validación y model binding.
Binding: de la ruta, query, body (JSON), form, headers. Validación: DataAnnotations ([Required], [Range]) con [ApiController] que responde 400 automático, o FluentValidation para reglas complejas, o —en el estilo envelope— validar en el servicio y devolver Result.Fail("Validation", ...) sin lanzar excepción.
EF Core vs Dapper vs ADO.NET puro. ¿Cuándo cada uno?
| Para qué | Costo | |
|---|---|---|
| EF Core | CRUD, cambio de esquema frecuente, productividad, tracking | Curva de queries traducidas, sorpresas de rendimiento, N+1 |
| Dapper | Consultas de lectura con SQL a mano, control total, alto rendimiento | Escribes el SQL y el mapeo; sin tracking ni migraciones |
| ADO.NET | Legacy, drivers ODBC, control absoluto, sin dependencias | Mucho boilerplate: SqlConnection/Command/Reader a mano |
SqlDataReader, que es lo que pide el rol de mantenimiento de Softtek."¿Qué es una transacción y cuándo la necesitas en el servicio?
Un grupo de operaciones que se aplica todo o nada (atomicidad). La necesitas cuando escribes en varias tablas y un fallo a mitad dejaría datos inconsistentes — p. ej. insertar un pedido y sus líneas. En Dapper: conn.BeginTransaction(), pasas la tx a cada Execute, Commit al final, Rollback en el catch. Niveles de aislamiento: READ COMMITTED (default de SQL Server) evita lecturas sucias; SERIALIZABLE para evitar phantom reads a costa de concurrencia.
¿Cómo pruebas una Web API?
Unitarias del servicio con xUnit/NUnit + Moq (mockeas la interfaz del repo). Integración con WebApplicationFactory<Program> — levanta la API en memoria y le pegas con HttpClient; para la BD, Testcontainers (SQL Server real en Docker) o SQLite en memoria. "El equipo usa SonarQube en el pipeline de Azure DevOps para cobertura y code smells."
SOLID — una línea cada uno, ver pestaña Arquitectura.
IDisposable / using — liberar recursos no gestionados (conexiones, archivos); await using para IAsyncDisposable.
Excepciones — no para control de flujo; captura específica; no tragues (catch {}); log con contexto; excepción custom por dominio.
IOptions<T> — binding tipado de appsettings.json; IOptionsSnapshot para recarga por request.
Autenticación — JWT bearer, claims, [Authorize(Roles=...)] o policies; refresh tokens.
Primary constructors (C# 12) — class Svc(IDb db); menos boilerplate para inyección.
span/Memory — solo si preguntan por rendimiento/alocación; probablemente fuera de nivel.
Result<T> 25 minDado un SKU, devolver en qué almacenes hay disponibilidad (tabla única StockPorAlmacen(Sku, Almacen, Cantidad)). El endpoint responde siempre 200 con el envelope.
Cantidad > 0 o hay columna de reservado? ¿typeError es string o enum? Read-back de las firmas antes de teclear."ProblemDetails (RFC 7807), con el 500 generado por UseExceptionHandler y no por un catch. Practica las dos y pregunta cuál usan antes de teclear. La versión REST está resuelta en el ejercicio «El mismo endpoint, pero en REST puro» del Dojo. El matiz que preguntan ahí: "existe pero sin stock" es 200 con lista vacía, no 404.// ---- Envelope ----
public class Result<T>
{
public bool ProcessCorrect { get; set; }
public string TypeError { get; set; } = "";
public string MessageError { get; set; } = "";
public T? Data { get; set; }
public static Result<T> Ok(T data) => new() { ProcessCorrect = true, Data = data };
public static Result<T> Fail(string type, string msg) =>
new() { ProcessCorrect = false, TypeError = type, MessageError = msg };
}
// ---- DTO ----
public sealed record DisponibilidadAlmacen(string Almacen, int Cantidad);
// ---- Servicio ----
public interface IInventarioService
{
Task<Result<IReadOnlyList<DisponibilidadAlmacen>>> PorSkuAsync(string sku, CancellationToken ct = default);
}
public sealed class InventarioService(IDbConnection db, ILogger<InventarioService> log) : IInventarioService
{
private const string Sql = """
SELECT s.Almacen, s.Cantidad
FROM dbo.StockPorAlmacen AS s
WHERE s.Sku = @Sku AND s.Cantidad > 0
ORDER BY s.Cantidad DESC;
""";
public async Task<Result<IReadOnlyList<DisponibilidadAlmacen>>> PorSkuAsync(
string sku, CancellationToken ct = default)
{
if (string.IsNullOrWhiteSpace(sku))
return Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("Validation", "El SKU es obligatorio.");
try
{
var cmd = new CommandDefinition(Sql, new { Sku = sku }, cancellationToken: ct);
var rows = (await db.QueryAsync<DisponibilidadAlmacen>(cmd)).AsList();
return rows.Count == 0
? Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("NotFound", $"Sin disponibilidad para {sku}.")
: Result<IReadOnlyList<DisponibilidadAlmacen>>.Ok(rows);
}
catch (SqlException ex)
{
log.LogError(ex, "Error consultando disponibilidad de {Sku}", sku);
return Result<IReadOnlyList<DisponibilidadAlmacen>>.Fail("Exception", "Error al consultar la disponibilidad.");
}
}
}
// ---- Endpoint (Minimal API) ----
app.MapGet("/api/productos/{sku}/disponibilidad",
async (string sku, IInventarioService svc, CancellationToken ct) =>
Results.Ok(await svc.PorSkuAsync(sku, ct)));
// ---- o Controller ----
[ApiController, Route("api/productos")]
public class ProductosController(IInventarioService svc) : ControllerBase
{
[HttpGet("{sku}/disponibilidad")]
public async Task<IActionResult> Disponibilidad(string sku, CancellationToken ct)
=> Ok(await svc.PorSkuAsync(sku, ct));
}
public async Task<Result<int>> CrearPedidoAsync(NuevoPedido input, CancellationToken ct)
{
if (input.Lineas is null or { Count: 0 })
return Result<int>.Fail("Validation", "El pedido necesita al menos una línea.");
using var conn = new SqlConnection(_cs);
await conn.OpenAsync(ct);
using var tx = conn.BeginTransaction();
try
{
var pedidoId = await conn.ExecuteScalarAsync<int>(new CommandDefinition(
"INSERT INTO dbo.[Order](Fecha, ClienteId) OUTPUT INSERTED.OrderId VALUES(SYSUTCDATETIME(), @ClienteId);",
new { input.ClienteId }, tx, cancellationToken: ct));
foreach (var l in input.Lineas)
await conn.ExecuteAsync(new CommandDefinition(
"INSERT INTO dbo.OrderLine(OrderId, ProductId, Cantidad, Precio) VALUES(@pedidoId, @ProductId, @Cantidad, @Precio);",
new { pedidoId, l.ProductId, l.Cantidad, l.Precio }, tx, cancellationToken: ct));
tx.Commit();
return Result<int>.Ok(pedidoId);
}
catch (Exception ex)
{
tx.Rollback();
_log.LogError(ex, "Rollback creando pedido");
return Result<int>.Fail("Exception", "No se pudo crear el pedido.");
}
}
OUTPUT INSERTED.OrderId me da el id sin un segundo round-trip. Batching con una tabla-valued parameter si son muchas líneas."public sealed record PageRequest(int Page = 1, int PageSize = 20, string? Search = null);
public sealed record PagedResponse<T>(IReadOnlyList<T> Data, long TotalCount, int Page, int TotalPages);
private const string Sql = """
SELECT p.Sku, p.Nombre, p.Precio
FROM dbo.Product p
WHERE (@Search IS NULL OR p.Nombre LIKE '%' + @Search + '%')
ORDER BY p.Nombre
OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
SELECT COUNT(*)
FROM dbo.Product p
WHERE (@Search IS NULL OR p.Nombre LIKE '%' + @Search + '%');
""";
public async Task<PagedResponse<ProductDto>> ListAsync(PageRequest req, CancellationToken ct)
{
using var multi = await _db.QueryMultipleAsync(new CommandDefinition(Sql, req, cancellationToken: ct));
var data = (await multi.ReadAsync<ProductDto>()).AsList();
var total = await multi.ReadSingleAsync<long>();
var pages = (int)Math.Ceiling(total / (double)req.PageSize);
return new(data, total, req.Page, pages);
}
OFFSET); TotalCount int vs long; OFFSET/FETCH vs keyset para tablas enormes; el LIKE '%x%' no usa índice — si lo mencionan, full-text search.public sealed class ExceptionToResultMiddleware(RequestDelegate next, ILogger<ExceptionToResultMiddleware> log)
{
public async Task Invoke(HttpContext ctx)
{
try { await next(ctx); }
catch (Exception ex)
{
log.LogError(ex, "Excepción no controlada en {Path}", ctx.Request.Path);
ctx.Response.StatusCode = StatusCodes.Status200OK; // estilo envelope
ctx.Response.ContentType = "application/json";
await ctx.Response.WriteAsJsonAsync(new
{
processCorrect = false,
typeError = "Exception",
messageError = "Ocurrió un error inesperado.",
});
}
}
}
// app.UseMiddleware<ExceptionToResultMiddleware>(); // lo más arriba posible
ProblemDetails y el status real (500). La idea es la misma: un solo lugar, nunca filtrar ex.Message ni el stack al cliente.""Dada una lista de ventas (sku, monto), devuelve el top 3 de SKUs por monto total."
public static IReadOnlyList<string> Top3PorMonto(IEnumerable<(string Sku, decimal Monto)> ventas) =>
ventas.GroupBy(v => v.Sku)
.Select(g => (Sku: g.Key, Total: g.Sum(x => x.Monto)))
.OrderByDescending(x => x.Total)
.Take(3)
.Select(x => x.Sku)
.ToList();
GroupBy + Sum es O(n). Si fuera streaming de millones y solo quiero top 3, un diccionario acumulador + un heap de tamaño 3 → O(n log 3). Para 3 elementos no vale la pena; lo menciono como el paso siguiente si el volumen lo pide."Tu CV no menciona SQL a fondo y es lo que más cae en prueba en vivo. Objetivo: escribir una consulta con JOIN + filtro + orden + paginación en < 4 min sin titubear, narrando.
Tipos de JOIN.
INNER (solo coincidencias), LEFT (todas las de la izquierda + null si no hay match — útil si "el producto pudo ser borrado"), RIGHT (raro, se reescribe como LEFT), FULL (ambos lados), CROSS (producto cartesiano). "Si un pedido puede tener un producto que ya no existe en catálogo, uso LEFT JOIN a Product y manejo el null; con INNER perdería esas filas silenciosamente."Índices: clustered, nonclustered, covering.
Clustered: ordena físicamente la tabla, uno por tabla (normalmente la PK). Nonclustered: estructura aparte con punteros a la fila. Covering: un nonclustered que con INCLUDE lleva todas las columnas que la consulta necesita, así no va a la tabla ("key lookup"). El WHERE y el JOIN definen qué indexar; el ORDER BY se beneficia si el índice ya viene ordenado así.
WHERE Sku = @Sku con SELECT Almacen, Cantidad pondría CREATE INDEX IX_Stock_Sku ON StockPorAlmacen(Sku) INCLUDE (Almacen, Cantidad) — seek + covering, sin lookups."¿Cómo diagnosticas una consulta lenta?
SET STATISTICS IO, TIME ON para ver lecturas lógicas y tiempo; el plan de ejecución (estimado y real) para ver scans vs seeks, key lookups, spills, y estimaciones de filas erradas (estadísticas viejas → UPDATE STATISTICS). Señales: table scan donde debería haber seek, warnings de conversión implícita (un parámetro con tipo distinto a la columna mata el índice).Rango de fechas: ¿por qué no BETWEEN?
BETWEEN '2026-01-01' AND '2026-01-31' se pierde todo lo del 31 después de medianoche si la columna es datetime. Usa semiabierto: Fecha >= @desde AND Fecha < @hasta donde @hasta es el primer día del mes siguiente. Funciona igual con date, datetime o datetime2.Paginación: OFFSET/FETCH vs keyset.
OFFSET n ROWS FETCH NEXT m es simple pero degrada en páginas altas (SQL Server igual lee y descarta las n filas). Keyset (seek method): WHERE (Fecha, Id) < (@ultimaFecha, @ultimoId) ORDER BY Fecha DESC, Id DESC FETCH NEXT m — usa el índice, constante en cualquier página, pero no permite "ir a la página 500" directo. Para infinite scroll, keyset; para un grid con números de página, OFFSET.Niveles de aislamiento / concurrencia.
READ COMMITTED (default): no lees cambios no confirmados. REPEATABLE READ: las filas que leíste no cambian en tu transacción. SERIALIZABLE: ni aparecen filas nuevas (no phantoms) — máximo aislamiento, mínima concurrencia. Para el "lost update" (dos usuarios editan lo mismo): optimistic concurrency con una columna rowversion/timestamp y WHERE RowVersion = @original en el UPDATE; si afecta 0 filas, alguien te ganó.N+1 en SQL.
IN con todos los ids de golpe. En EF: Include o proyección con Select.Stored procedures: ¿sí o no?
SELECT s.Almacen, s.Cantidad
FROM dbo.StockPorAlmacen AS s
WHERE s.Sku = @Sku
AND s.Cantidad > 0
ORDER BY s.Cantidad DESC;
Variantes: solo ubicaciones → SELECT DISTINCT s.Almacen. Total consolidado → SELECT SUM(s.Cantidad) ... (confirma primero). Varios SKUs → WHERE s.Sku IN @Skus.
SELECT ol.OrderLineId, o.OrderId, o.OrderDate,
p.Sku, p.Nombre, ol.Cantidad, ol.Precio,
(ol.Cantidad * ol.Precio) AS Importe
FROM dbo.OrderLine AS ol
JOIN dbo.[Order] AS o ON o.OrderId = ol.OrderId
JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
WHERE o.OrderDate >= @Desde
AND o.OrderDate < @Hasta
ORDER BY o.OrderDate DESC, ol.OrderLineId DESC
OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
SELECT COUNT(*)
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
WHERE o.OrderDate >= @Desde AND o.OrderDate < @Hasta;
SELECT TOP (5)
p.Sku, p.Nombre,
SUM(ol.Cantidad) AS Unidades,
SUM(ol.Cantidad * ol.Precio) AS Importe
FROM dbo.OrderLine AS ol
JOIN dbo.Product AS p ON p.ProductId = ol.ProductId
GROUP BY p.Sku, p.Nombre
ORDER BY Importe DESC;
Con empate controlado: ROW_NUMBER() OVER (ORDER BY SUM(...) DESC) en una CTE y WHERE rn <= 5.
SELECT p.Sku, p.Nombre
FROM dbo.Product AS p
WHERE NOT EXISTS (
SELECT 1
FROM dbo.OrderLine ol
JOIN dbo.[Order] o ON o.OrderId = ol.OrderId
WHERE ol.ProductId = p.ProductId
AND o.OrderDate >= @DesdeMes
);
NOT EXISTS en vez de LEFT JOIN ... WHERE x IS NULL: se lee mejor y el optimizador lo resuelve como anti-semi-join, corta en la primera coincidencia."SELECT p.Sku, p.Nombre, p.Precio
FROM dbo.Product AS p
WHERE p.Nombre LIKE '%' + @q + '%'
OR p.Sku LIKE @q + '%' -- prefijo en SKU sí usa índice
ORDER BY p.Nombre;
"LIKE '%x%' con comodín al inicio no usa índice — scan. Si el volumen lo exige, Full-Text Search con CONTAINS. En SKU el patrón de prefijo 'x%' sí aprovecha el índice."
UPDATE dbo.StockPorAlmacen
SET Cantidad = Cantidad - @Cantidad
WHERE Sku = @Sku AND Almacen = @Almacen
AND Cantidad >= @Cantidad; -- no dejar negativo
IF @@ROWCOUNT = 0
THROW 50001, 'Stock insuficiente o no encontrado', 1;
WHERE ... AND Cantidad >= @Cantidad hace el decremento atómico: dos descuentos concurrentes no pueden bajar de cero porque la condición se evalúa en el momento del update. Reviso @@ROWCOUNT para saber si aplicó."SELECT con lo que piden … FROM la tabla base … JOIN las que necesito y por qué … WHERE con parámetros, rango semiabierto … ORDER BY con desempate … paginación al final."En prueba en vivo o pair te pueden pedir "crea el proyecto", "haz un branch", "corre los tests". Ten estos en los dedos.
| Necesito | Comando |
|---|---|
| Rama nueva desde develop | git switch -c feature/DEV-123-descripcion develop |
| Traer cambios de la base sin merge commit | git pull --rebase origin develop |
| Guardar sin commitear | git stash / git stash pop |
| Deshacer el último commit, conservar cambios | git reset --soft HEAD~1 |
| Descartar cambios locales de un archivo | git restore ruta/archivo |
| Revertir un commit ya publicado | git revert <hash> (crea commit inverso, no reescribe historia) |
| Traer un commit puntual de otra rama | git cherry-pick <hash> |
| Encontrar qué commit rompió algo | git bisect start / bad / good <hash> |
| Recuperar algo "perdido" | git reflog → git reset --hard <hash> |
| Limpiar commits antes de PR | git rebase -i develop (squash/fixup/reword) |
--soft mueve HEAD (cambios quedan staged) · --mixed (default) los deja sin stage · --hard los borra. Merge vs rebase: merge preserva la historia real (merge commit); rebase la reescribe lineal — nunca rebase de una rama compartida ya publicada.ng new mi-app --standalone --routing --style=scss --ssr=false
ng generate component productos/product-search # o: ng g c ...
ng g service core/product-search
ng g guard core/auth
ng g interceptor core/api
ng add @angular/material
ng serve -o # dev server + abre browser
ng build --configuration=production
ng test # Karma/Jasmine
ng test --watch=false --code-coverage
ng lint
ng update @angular/core @angular/cli
ng g c x --dry-run para ver qué crearía sin escribir. --inline-template --inline-style para un componente de un archivo (útil en prueba en vivo).
dotnet new webapi -n Tienda.Api --use-controllers # o minimal: sin la flag
dotnet new sln -n Tienda && dotnet sln add Tienda.Api
dotnet new xunit -n Tienda.Tests
dotnet add Tienda.Api package Dapper
dotnet add Tienda.Api package Microsoft.Data.SqlClient
dotnet add Tienda.Tests reference Tienda.Api
dotnet build
dotnet run --project Tienda.Api
dotnet test
dotnet watch --project Tienda.Api run # hot reload
# EF (si el proyecto lo usa)
dotnet tool install --global dotnet-ef
dotnet ef migrations add Inicial
dotnet ef database update
npm ci # instala exacto del lock (CI); npm install para cambiar deps
npm run start / build / test
npm outdated / npm update
npx <paquete> # ejecutar sin instalar global
node --version / nvm use 20
sqlcmd -S localhost\SQLEXPRESS -E -d Tienda -Q "SELECT COUNT(*) FROM Product;"
sqlcmd -S localhost\SQLEXPRESS -E -i script.sql # ejecutar archivo
# -E = Windows auth; -U/-P para usuario/contraseña
az repos pr create --source-branch feature/x --target-branch develop --title "..." --work-items 123
az pipelines run --branch feature/x
az boards work-item show --id 123
| Principio | Qué es | Ejemplo tuyo |
|---|---|---|
| Single Responsibility | Una clase, una razón para cambiar | El interceptor hace manejo de error; el guard hace sesión/roles — no lo mismo |
| Open/Closed | Abierto a extensión, cerrado a modificación | Nueva implementación de ProductSearchService sin tocar el componente |
| Liskov | Un subtipo debe poder sustituir al base sin romper | Cualquier IInventarioService cumple el contrato: mismos tipos de retorno y errores |
| Interface Segregation | Interfaces chicas y específicas, no una gigante | Un repo de lectura y uno de escritura separados en vez de IRepository con 20 métodos |
| Dependency Inversion | Depender de abstracciones, no de concreto | El componente inyecta PRODUCT_SEARCH (token), no HttpClient |
Interfaz (contrato tipado) → Service (HttpClient, headers, mapeo transporte) → Assembly (arma request desde el modelo de dominio / desarma la respuesta) → Promise (orquesta varias llamadas, expone al componente una API limpia basada en promesas). "Lo diseñé para consumir 4 microservicios .NET distintos —lealtad, cotizador, clientes, CFDI— sin que cada componente conociera la forma de cada backend. Si un servicio cambia su contrato, el golpe queda contenido en Service+Assembly."
GET /productos/{sku}/disponibilidad, no /getDisponibilidad.Result<T> si el equipo lo usa — di que sigues la convención del equipo).GET, PUT, DELETE idempotentes; POST no. Para reintentos seguros en POST, una idempotency key./v1/ en la ruta o header.Controller (HTTP) → Application/Business (casos de uso, orquestación, validación) → Domain (entidades, reglas) → Infrastructure (repos, EF/Dapper, servicios externos). La dependencia apunta hacia adentro: el dominio no conoce a EF. En Seus: "Controllers–Business–DTO sobre AWS Lambda."
"Diséñame la API para el catálogo de productos con búsqueda y stock."
GET /v1/productos?search=&page=&pageSize= → PagedResponse<ProductDto>GET /v1/productos/{sku} → detalleGET /v1/productos/{sku}/disponibilidad → por almacén"¿Cómo manejas configuración y secretos?"
appsettings.json + appsettings.{Environment}.json, IOptions<T> tipado, User Secrets en local, y variables de entorno / un vault (AWS Secrets Manager, Azure Key Vault) en prod. Nunca secretos en el repo.Nivel real del rol: no LeetCode hard. Estructuras básicas, Big-O, y resolver un problema tipo "agrupa / cuenta / encuentra" narrando el enfoque.
| Notación | Ejemplo |
|---|---|
| O(1) | Acceso a un Dictionary/Map por clave, push/pop |
| O(log n) | Búsqueda binaria, operaciones en árbol balanceado |
| O(n) | Recorrer una lista, un GroupBy |
| O(n log n) | Ordenar (Array.Sort, .sort()) |
| O(n²) | Doble bucle anidado — bandera roja, casi siempre hay un hash que lo baja a O(n) |
public static char? PrimeraNoRepetida(string s)
{
var conteo = new Dictionary<char, int>();
foreach (var c in s) conteo[c] = conteo.GetValueOrDefault(c) + 1;
foreach (var c in s) if (conteo[c] == 1) return c;
return null;
}
public static bool ExistePar(int[] nums, int objetivo)
{
var vistos = new HashSet<int>();
foreach (var n in nums)
{
if (vistos.Contains(objetivo - n)) return true;
vistos.Add(n);
}
return false;
}
var porCliente = pedidos
.GroupBy(p => p.ClienteId)
.ToDictionary(g => g.Key, g => g.Sum(p => p.Total));
En JS/TS: Object.groupBy(pedidos, p => p.clienteId) o reduce con un Map.
public static Node? Invertir(Node? head)
{
Node? prev = null;
while (head is not null)
{
var siguiente = head.Next;
head.Next = prev;
prev = head;
head = siguiente;
}
return prev;
}
Recursión vs iteración.
Situación · Tarea · Acción · Resultado. Cada respuesta: 60–90 s, con una cifra al final. Rellena los [huecos] con datos reales antes de la entrevista.
Todo tu material gira en torno a Seus Web, un producto comercial de una empresa real. Contar de más puede violar el NDA que firmaste — y un entrevistador con criterio lo nota y lo penaliza: si aireas lo de tu empleo anterior, airearás lo suyo.
| ✅ Cuenta sin problema | ❌ No cuentes |
|---|---|
| Arquitectura y decisiones técnicas · stack y versiones · el problema en abstracto · tu rol y tus commits · métricas tuyas (86 commits, 18+ módulos) | Nombres de clientes o cadenas de farmacias · cifras de negocio (ventas, márgenes, usuarios) · código o capturas · credenciales o endpoints internos · fallos concretos que dejen mal a la empresa |
La fórmula segura: describe el problema y tu solución, no los datos del negocio. "Un punto de venta para retail farmacéutico con más de 18 módulos" dice todo lo que necesitan sin nombrar a nadie.
Sobre los bancos de preguntas (incluido este): no te aprendas las respuestas de memoria. En un simulacro real, el entrevistador detectó al candidato porque repitió la definición casi palabra por palabra dos veces y le preguntó si estaba leyendo. Entiende qué quieren saber con cada pregunta y construye la respuesta con tus palabras y tu caso.
S: El producto era un monolito Angular de 18+ módulos corriendo en 86 sucursales; los builds y despliegues eran lentos y un cambio en un módulo obligaba a re-desplegar todo — con el riesgo que eso implica en un sistema del que dependen +500 terminales para vender.
T: Migrar los módulos de backoffice a una arquitectura de microfrontends sin frenar el desarrollo de features.
A: Elegí Native Federation por el stack moderno (esbuild). Migré compra directa, recepción electrónica, monitor de embarques y pedido manual a Angular 20; creé una librería Angular compartida para no duplicar; definí el contrato host–remotos y el versionado de la librería.
R: Despliegues independientes por módulo. Fui el mayor contribuidor del repo con 86 commits. [tiempo de build antes/después si lo tienes]
S: Cada módulo del front capturaba errores HTTP a su manera; comportamiento inconsistente, sesiones que no se cerraban bien.
T: Unificar el manejo de errores y de sesión en toda la app sin romper los módulos existentes.
A: Interceptor HTTP único + guards de sesión y roles; migré el flujo disperso a uno basado en promesas; fui módulo por módulo validando. Aproveché para cerrar fugas de suscripciones viejas.
R: Un solo punto de manejo de error; [nº de módulos tocados]; base para el cifrado de storage que vino después.
S: [elige: folios en compra directa / numerología de piezas en recepción electrónica / CORS en las APIs de facturación] — fallaba en producción y afectaba la operación de farmacias.
T: Encontrar la causa y corregir sin romper el resto del flujo.
A: Reproduje con datos reales, aislé [la causa], corregí y agregué [validación / prueba] para que no volviera.
R: Operación restablecida [en X tiempo]; el defecto no reapareció.
S: Datos sensibles de operación farmacéutica se guardaban en claro en el localStorage del navegador.
T: No estaba asignado, lo noté yo. Propuse cifrar el almacenamiento del cliente.
A: Diseñé el StorageEncryptionService (work item DEV-18726), lo integré en el flujo de sesión y migré los datos existentes.
R: Los datos sensibles dejaron de estar en claro en el navegador. Riesgo cerrado antes de que fuera un incidente.
S: Había que consumir 4 microservicios .NET distintos desde el front, cada uno con su forma.
T: Que los componentes no se acoplaran a cada backend.
A: Diseñé el patrón Interfaz–Service–Assembly–Promise; lo documenté y lo adoptó el equipo.
R: Cambios de contrato de un servicio quedan contenidos en 2 capas; los componentes no se enteran.
| Pregunta | Historia |
|---|---|
| "Cuéntame de un proyecto del que estés orgulloso" | 1 o 5 |
| "Una vez que lideraste sin ser el líder formal" | 1 o 2 |
| "Un error que cometiste / algo que salió mal" | [preparar una honesta: una decisión técnica que reconsideraste] |
| "Trabajo bajo presión / un deadline difícil" | 3 |
| "Iniciativa / algo que hiciste sin que te lo pidieran" | 4 |
| "Un desacuerdo técnico con un compañero" | [preparar: cómo lo resolviste con datos, no con ego] |
| "Por qué dejaste / estás dejando tu empleo" | Ver Guion — respuesta neutra, sin quejarse |
La escala llega en la segunda frase, antes que cualquier tecnología. "86 sucursales y más de 500 terminales" te reclasifica: dejas de ser alguien que sabe Angular y pasas a ser alguien que mantuvo un sistema de misión crítica a escala nacional. Eso es lo que justifica un sueldo de 45–60k.
La frase "si el punto de venta se cae, las sucursales dejan de vender" hace el trabajo de diez adjetivos: comunica la presión real bajo la que trabajabas sin que tengas que decir "trabajo bien bajo presión".
Y el orden punto de venta → backoffice cuenta una progresión: entraste al core transaccional y luego te dieron la migración arquitectónica.
De una sesión de reclutadores evaluando pitches reales de candidatos en vivo:
El punto 3 era tu hueco y ya está resuelto. Tu escala real:
| Dato | Cómo se dice |
|---|---|
| 86 sucursales en toda la República | ✅ Dilo tal cual. Es alcance de despliegue, es técnico y es impresionante. |
| +500 terminales (5–6 cajas + 1 estación de pedidos por sucursal) | ✅ Dilo tal cual. Habla de la escala del sistema, no del negocio. |
| ~3 millones de pesos diarios | ⚠️ Di "del orden de millones de pesos en transacciones al día". La cifra exacta es la facturación de un tercero. |
| El nombre de la cadena | ❌ Nunca. "Retail farmacéutico" basta. |
Prepara 2–3 frases con algo concreto de la empresa (su producto, su stack, su etapa). Molde:
| Situación | Qué dices |
|---|---|
| Te preguntan primero el rango | "Mi rango objetivo está entre 35 y 60 mil MXN mensuales, según el alcance del rol y las prestaciones. ¿Cuál es la banda que tienen para la posición?" |
| 100% remoto y todo lo demás encaja | Piso real 27 mil, pero no lo ancles ahí: pide 35+. |
| Insisten en un número | Da el objetivo (45–50k), no el piso. Siempre puedes bajar, nunca subir. |
| Ofrecen por debajo de 27k | "Está por debajo de lo que puedo considerar para un rol de este nivel." |
Nunca: quejarte del jefe, del sueldo, de los compañeros o de la empresa. Neutro y hacia adelante.
"Disponibilidad inmediata." Remoto, con base en [Ciudad de México — según la decisión de las postulaciones].
Auto-intro en inglés lista → pestaña Inglés.
Para procesos donde el inglés es "deseable" o hay un reclutador que suelta un par de preguntas en inglés. Si exigen C1 conversacional, es filtro duro — no fuerces.
| Situación | Frase |
|---|---|
| No entendiste | "Could you rephrase that, please?" / "Sorry, could you say that more slowly?" |
| Ganar tiempo | "That's a good question. Let me think for a second." |
| Confirmar que entendiste | "So, if I understand correctly, you're asking about [X]. Is that right?" |
| Ser honesto del nivel | "My spoken English is intermediate — I'm more comfortable reading and writing. I can follow technical discussions and I'm improving it actively." |
| Cerrar una respuesta | "Does that answer your question, or would you like me to go deeper?" |
| despliegue | deployment / to ship |
| rama / fusionar | branch / to merge |
| pruebas unitarias | unit tests · cobertura = coverage |
| deuda técnica | technical debt |
| rendimiento | performance · cuello de botella = bottleneck |
| mantenibilidad | maintainability |
| a gran escala | at scale · alta disponibilidad = high availability |
| requisito | requirement · aclarar alcance = clarify scope |
"Tell me about a challenging project."
"Why do you want to work remotely?"
GitHub Copilot: Disable Globally) y la extensión de Claude si la prueba lo prohíbe — cada vez lo piden más.dotnet --version, ng version, SQL Server arriba (localhost\SQLEXPRESS).dotnet new webapi, ng new).switchMap."| Trampa | Defensa |
|---|---|
| La tarjeta decía "remoto", el rol es híbrido | Confirmar modalidad con el reclutador antes de invertir tiempo |
| "¿Cuántos años de X?" en el formulario | Angular=5, C#=5, REST=5, .NET Framework=2, Design Systems=2, inglés=Básico |
| Piden 8+ años y tú tienes 5 | La carta lo aclara; en entrevista: "Cinco años pero muy densos, un solo producto a gran escala" |
| Piden título expedido | "Pasante de ITICS, titulación en trámite" — no mentir |
| "Sin EF / sin stored procedures" | SQL a mano + Dapper. Ver pestañas SQL y .NET escrito |
| Prueba que parece hablada pero piden teclear | Ten los ejercicios escritos practicados con las manos, no solo leídos |
| Angular | Avanzado |
| APIs REST | Avanzado |
| Git | Avanzado |
| C# / .NET Core | Avanzado |
| SQL Server | Intermedio |
| Docker / OWASP | Básico (teórico, no aplicado en el trabajo) |
| Inglés | Básico |
| Ciclo de vida de proyecto | Avanzado (Azure DevOps end to end) |
1. Sin "creo/siento/pienso" — seguridad. 2. Cada respuesta con contexto de Seus. 3. Término técnico exacto.
Afirmación directa → caso Seus → término exacto → matiz.
Salario: pido 45–50k, objetivo 35–60k, piso 27k solo si 100% remoto. Experiencia: 5 años. Disponibilidad: inmediata.
debounceTime + distinctUntilChanged + switchMap.exhaustMap. Guardados en orden → concatMap.>= desde AND < hasta), nunca BETWEEN.ORDER BY con desempate por id.DbContext/repos = Scoped. Config/cache = Singleton.Result<T>: processCorrect / typeError / data, HTTP 200 siempre, validación sube al servicio (no excepción).InjectionToken, no HttpClient directo.WHERE x + SELECT a,b → INDEX(x) INCLUDE(a,b) covering.1) Migración a microfrontends. 2) Estandarización de manejo de errores. 3) StorageEncryptionService por iniciativa propia.
· ¿Cómo es el ciclo de ramas/PR/despliegue? · ¿El mayor reto técnico del equipo ahora? · ¿Hay camino hacia arquitectura/liderazgo?
· "Creo que…" · quejas del empleo anterior · que sé algo que no sé · números por debajo de mi objetivo · quedarme callado tecleando.