Full Stack .
NET + Angular
Interview Mastery Guide — 2025 Edition
Calibrated for 3.5–4 Years Experience | Senior & Mid-Senior Roles
Prepared for Ujjwal Rana
Section 1: C# & .NET Internals
At 3.5+ years, interviewers skip basic OOP. They go straight to: memory model, async state
machines, GC internals, and type system edge cases. Every Q&A below reflects what's actually
asked at MNCs and product companies.
1.1 Memory Model — Stack vs Heap
Q: What lives on the stack vs heap in C#? Give real examples.
Answer:
The stack stores value types (int, bool, struct, char) and references themselves. The heap stores
reference type objects (class instances, arrays, strings). When you declare int x = 5, the value 5 lives
on the stack. When you say var p = new Person(), the reference p is on the stack but the Person object
lives on the heap.
A critical nuance: value types inside a class are stored on the heap as part of the object, not on the
stack.
// Stack allocation
int age = 30; // value on stack
bool isActive = true; // value on stack
// Heap allocation
var person = new Person(); // reference on stack, object on heap
int[] scores = new int[5]; // reference on stack, array on heap
// Value type inside class -> on heap
class Order {
public int Quantity; // int on heap because it belongs to Order
}
Interview trap: 'struct is always on stack' is FALSE. A struct field inside a class lives on the heap
with the class. Also, boxing a value type (int -> object) copies it to the heap — this has a GC cost.
1.2 Garbage Collector — Generational GC
Q: How does the .NET Garbage Collector work? What are Gen0, Gen1, Gen2?
Answer:
The .NET GC is a generational, mark-and-compact collector. It divides the heap into three generations
based on the observation that most objects die young (the 'generational hypothesis').
• Gen0: Newly allocated, short-lived objects. Collected very frequently. Fast collection
(milliseconds). Example: local variables, temporary strings inside a loop.
• Gen1: Objects that survived one Gen0 collection. A buffer between short and long-lived objects.
Collected less often than Gen0.
• Gen2: Long-lived objects. Collected least often. Examples: static data, application-level caches,
large singleton objects.
• Large Object Heap (LOH): Objects >= 85,000 bytes go here. NOT compacted by default — can
cause fragmentation. In .NET 4.5.1+, you can request compaction manually.
// Triggering a GC (don't do this in production)
[Link](0); // Collect Gen0 only
[Link](); // Collect all generations
// Check generation of an object
var obj = new MyClass();
[Link]([Link](obj)); // 0
[Link]();
[Link]([Link](obj)); // 1 (survived one collection)
Senior insight: Frequent LOH allocations cause GC pressure. If you're allocating large byte arrays
repeatedly (e.g., for file processing), use ArrayPool<byte> to rent and return buffers instead. This is
the answer to 'how do you reduce GC pressure in high-throughput services'.
1.3 IDisposable, using, and Finalizers
Q: What is the difference between Dispose() and a finalizer? When do you use each?
Answer:
[Link]() is deterministic cleanup — you call it explicitly (or via using), and resources are
released immediately. A finalizer (~ClassName()) is non-deterministic — the GC calls it at an unknown
time before reclaiming the object.
The standard pattern is: implement IDisposable for managed cleanup (closing streams, DB
connections), and optionally add a finalizer as a safety net for unmanaged resources.
public class FileProcessor : IDisposable
{
private FileStream _stream;
private bool _disposed = false;
public FileProcessor(string path)
=> _stream = new FileStream(path, [Link]);
public void Dispose()
{
Dispose(true);
[Link](this); // Tell GC: no need to call finalizer
}
protected virtual void Dispose(bool disposing)
{
if (_disposed) return;
if (disposing)
_stream?.Dispose(); // Managed resource
_disposed = true;
}
~FileProcessor() // Finalizer — safety net only
=> Dispose(false);
}
// Usage — using ensures Dispose() is called even on exception
using var processor = new FileProcessor("[Link]");
1.4 Async/Await — The State Machine
Q: What actually happens when you write 'await' in C#? What does the compiler generate?
Answer:
When you await a Task, the C# compiler rewrites your async method into a state machine. The method
is split at every await point. The current state is saved, the thread is freed to do other work, and when
the awaited Task completes, execution resumes from where it left off — possibly on a different thread.
// What you write:
public async Task<string> GetDataAsync()
{
var response = await _httpClient.GetAsync(url);
var content = await [Link]();
return content;
}
// What the compiler generates (simplified concept):
// A struct implementing IAsyncStateMachine with states 0, 1, 2
// State 0: Start GetAsync, register continuation
// State 1: GetAsync completed, start ReadAsStringAsync
// State 2: ReadAsStringAsync completed, return result
Q: What is ConfigureAwait(false) and when should you use it?
Answer:
By default, await captures the current SynchronizationContext (e.g., UI thread context, [Link]
request context) and resumes on it. ConfigureAwait(false) tells the runtime: 'don't bother resuming on
the original context, use any available thread'. This avoids deadlocks and improves throughput in library
code.
// Library code — always use ConfigureAwait(false)
public async Task<Data> FetchAsync(string url)
{
var result = await _client.GetAsync(url).ConfigureAwait(false);
return await [Link]<Data>().ConfigureAwait(false);
}
// Application code (controllers, UI) — omit ConfigureAwait
// because you often DO need the context (HttpContext, UI thread)
public async Task<IActionResult> Get()
{
var data = await _service.FetchAsync(url); // Context matters here
return Ok(data);
}
Q: Demonstrate a deadlock caused by .Result on an async method.
Answer:
// DEADLOCK — DO NOT DO THIS
public string GetData()
{
// .Result blocks the thread AND holds the SynchronizationContext
// The async method tries to resume on that same context -> DEADLOCK
return GetDataAsync().Result; // Hangs forever
}
// FIX: Go async all the way
public async Task<string> GetData()
{
return await GetDataAsync(); // No deadlock
}
1.5 Task vs ValueTask
Q: What is ValueTask and when should you prefer it over Task?
Answer:
Task is a reference type — it's always allocated on the heap. For methods that frequently complete
synchronously (cache hits, simple lookups), this heap allocation is wasteful. ValueTask is a struct that
avoids the allocation when the result is already available.
// Task — allocates even on cache hit
public async Task<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
return user; // Still allocates a Task wrapper
return await _db.[Link](id);
}
// ValueTask — no allocation on cache hit
public ValueTask<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
return [Link](user); // No heap allocation
return new ValueTask<User>(FetchFromDbAsync(id));
}
// RULE: ValueTask is for hot paths where sync completion is common.
// Never await a ValueTask more than once.
1.6 Generics and Constraints
Q: What are generic constraints in C# and why do they matter?
Answer:
Generic constraints let you restrict the types that can be used with a generic type parameter. Without
constraints, you can only call methods defined on object. With constraints, you get access to members
of the constrained type.
// Without constraints — very limited
public T Process<T>(T item)
{
// Can only call .ToString(), .Equals(), .GetHashCode()
return item;
}
// With interface constraint
public T Validate<T>(T entity) where T : IValidatable, new()
{
[Link](); // Can call IValidatable methods
return entity;
}
// Common constraints:
where T : class // Reference type
where T : struct // Value type
where T : new() // Has parameterless constructor
where T : BaseClass // Inherits from BaseClass
where T : IInterface // Implements interface
where T : notnull // Non-nullable
1.7 Records, Pattern Matching, and Modern C#
Q: What are records and when do you use them over classes?
Answer:
Records are reference types (record class) or value types (record struct) with built-in value equality,
immutability by default, and a concise declaration syntax. Use records for data transfer objects (DTOs),
command/query models, and anywhere you need value-based equality without writing
Equals/GetHashCode manually.
// Class — reference equality by default
class ProductDto { public string Name { get; set; } public decimal Price { get; set; } }
var a = new ProductDto { Name = "Pen", Price = 10 };
var b = new ProductDto { Name = "Pen", Price = 10 };
[Link](a == b); // False (different references)
// Record — value equality built-in
record ProductDto(string Name, decimal Price);
var a = new ProductDto("Pen", 10);
var b = new ProductDto("Pen", 10);
[Link](a == b); // True
// Non-destructive mutation with 'with'
var updated = a with { Price = 12 };
// Perfect for CQRS commands/queries
record CreateOrderCommand(Guid CustomerId, List<OrderItem> Items);
Section 2: [Link] Core — Deep Dive
2.1 Middleware Pipeline
Q: Explain the [Link] Core middleware pipeline. How does ordering matter?
Answer:
The middleware pipeline is a sequence of delegates that process HTTP requests and responses. Each
middleware calls next() to pass control to the next component. If it doesn't call next(), the pipeline short-
circuits. Request flows in, response flows out — like a Russian nesting doll.
// [Link] — order is critical
[Link]("/error"); // 1st — catches all downstream exceptions
[Link](); // 2nd
[Link](); // 3rd — short-circuits for static files
[Link](); // 4th — matches route
[Link](); // 5th — who are you?
[Link](); // 6th — what can you do?
[Link](); // 7th — execute controller action
// Custom middleware
public class TimingMiddleware
{
private readonly RequestDelegate _next;
public TimingMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context)
{
var sw = [Link]();
await _next(context); // Call next middleware
[Link]();
[Link]["X-Response-Time"] = $"{[Link]}ms";
}
}
[Link]<TimingMiddleware>();
Interview insight: If UseAuthentication() comes AFTER UseAuthorization(), authorization always
fails because the identity hasn't been established yet. This ordering mistake causes real production
bugs.
2.2 Dependency Injection — Lifetimes
Q: What is the difference between Singleton, Scoped, and Transient? What's a captive
dependency?
Answer:
Dependency Injection lifetimes control when an instance is created and how long it lives:
• Singleton: One instance for the entire application lifetime. Shared across all requests and all
threads. Use for: configuration services, memory caches, HttpClient factories.
• Scoped: One instance per HTTP request (or per scope). Disposed when the request ends. Use
for: DbContext, unit-of-work, services that should be consistent within one request.
• Transient: A new instance every time it is requested. Use for: lightweight, stateless services.
// Registration
[Link]<IEmailConfig, EmailConfig>();
[Link]<IOrderRepository, OrderRepository>();
[Link]<IEmailSender, SmtpEmailSender>();
// CAPTIVE DEPENDENCY — the most common DI mistake
// Singleton injecting a Scoped service — BROKEN
public class OrderCache // Singleton
{
private readonly IOrderRepository _repo; // Scoped!
public OrderCache(IOrderRepository repo) => _repo = repo;
// _repo is captured at startup and never released.
// After first request ends, _repo is disposed but OrderCache still holds it.
// Next call -> ObjectDisposedException
}
// FIX: Use IServiceScopeFactory to create a scope manually
public class OrderCache
{
private readonly IServiceScopeFactory _factory;
public OrderCache(IServiceScopeFactory factory) => _factory = factory;
public async Task RefreshAsync()
{
using var scope = _factory.CreateScope();
var repo = [Link]<IOrderRepository>();
await [Link]();
}
}
2.3 Minimal APIs vs Controllers
Q: When would you choose Minimal APIs over Controllers in [Link] Core?
Answer:
Minimal APIs (introduced in .NET 6) are ideal for microservices and simple endpoints where the
overhead of MVC conventions (routing attributes, model binding conventions, filters) isn't needed. They
have less ceremony and better performance for high-throughput scenarios. Controllers are better for
large APIs with complex validation, filters, and versioning needs.
// Minimal API — clean, fast
[Link]("/orders/{id}", async (int id, IOrderService svc)
=> await [Link](id) is Order o ? [Link](o) : [Link]());
[Link]("/orders", async (CreateOrderRequest req, IOrderService svc) =>
{
var order = await [Link](req);
return [Link]($"/orders/{[Link]}", order);
});
// Controller — better for complex APIs
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
[Authorize(Roles = "Admin,Manager")]
[ProducesResponseType(typeof(OrderDto), 200)]
public async Task<IActionResult> GetOrder(int id) { ... }
}
2.4 Global Error Handling
Q: How do you implement centralized error handling in [Link] Core?
Answer:
There are three main approaches, each with different use cases:
// Approach 1: IExceptionHandler ([Link] Core 8+ — preferred)
public class GlobalExceptionHandler : IExceptionHandler
{
public async ValueTask<bool> TryHandleAsync(
HttpContext context, Exception ex, CancellationToken ct)
{
var (status, title) = ex switch
{
NotFoundException => (404, "Resource not found"),
ValidationException => (400, "Validation failed"),
UnauthorizedException => (401, "Unauthorized"),
_ => (500, "Internal server error")
};
[Link] = status;
await [Link](new ProblemDetails
{
Title = title, Status = status,
Detail = [Link]
}, ct);
return true; // Exception handled
}
}
// Registration
[Link]<GlobalExceptionHandler>();
[Link]();
2.5 Entity Framework Core — Deep Dive
Q: Explain the N+1 problem in EF Core and how you fix it.
Answer:
N+1 occurs when you load a collection of entities (1 query) and then access a navigation property for
each entity without eager loading, causing N additional queries — one per entity. This is a performance
killer at scale.
// N+1 PROBLEM — DO NOT DO THIS
var orders = await _context.[Link](); // Query 1
foreach (var order in orders)
{
var customer = [Link]; // Query 2, 3, 4... for each order
[Link]([Link]);
}
// FIX 1: Eager loading with Include
var orders = await _context.Orders
.Include(o => [Link]) // JOIN in single query
.Include(o => [Link])
.ThenInclude(i => [Link]) // Nested include
.ToListAsync();
// FIX 2: Projection — only fetch what you need
var orderDtos = await _context.Orders
.Select(o => new OrderDto(
[Link],
[Link],
[Link]
))
.ToListAsync();
Q: When do you use AsNoTracking() and why?
Answer:
AsNoTracking() tells EF Core not to track the returned entities in its change tracker. This means it won't
detect changes to those objects and won't include them in SaveChanges(). Use it for read-only queries
to improve performance significantly — especially in APIs returning data without modification.
// Read-only API endpoint — use AsNoTracking
var products = await _context.Products
.AsNoTracking() // No change tracking overhead
.Where(p => [Link])
.OrderBy(p => [Link])
.ToListAsync();
// DO track when you plan to modify
var order = await _context.[Link](id); // Tracked
[Link] = [Link];
await _context.SaveChangesAsync(); // EF detects the change -> UPDATE
Your resume mentions 60% query speed improvement. This is where AsNoTracking(), proper
indexing strategy, and avoiding N+1 come together. Be ready to walk through exactly what you
optimized.
Section 3: Architecture & Design Patterns
3.1 SOLID Principles with Real Code
Single Responsibility Principle
Q: Show a SOLID violation and how you'd fix it.
Answer:
// VIOLATION — OrderService does too many things
public class OrderService
{
public void ProcessOrder(Order order)
{
// Validate
if ([Link] == 0) throw new Exception("Empty order");
// Save to DB
_context.[Link](order);
_context.SaveChanges();
// Send email
var smtp = new SmtpClient();
[Link]("order@[Link]", [Link], "Order confirmed", "");
// Log
[Link]("[Link]", $"Order {[Link]} processed");
}
}
// FIX — each class has one responsibility
public class OrderValidator { public void Validate(Order o) { ... } }
public class OrderRepository { public void Save(Order o) { ... } }
public class OrderNotifier { public void Notify(Order o) { ... } }
public class OrderService
{
public void ProcessOrder(Order order)
{
_validator.Validate(order);
_repository.Save(order);
_notifier.Notify(order);
}
}
3.2 CQRS with MediatR
Q: Explain CQRS and implement it with MediatR in .NET.
Answer:
CQRS (Command Query Responsibility Segregation) separates read and write operations into different
models. Commands change state and return nothing (or just an ID). Queries read state and never
modify it. This separation enables independent scaling, different validation logic, and cleaner code
organization.
// 1. Define the Command
public record CreateOrderCommand(Guid CustomerId, List<OrderItem> Items)
: IRequest<Guid>;
// 2. Define the Handler
public class CreateOrderHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _repo;
public CreateOrderHandler(IOrderRepository repo) => _repo = repo;
public async Task<Guid> Handle(
CreateOrderCommand cmd, CancellationToken ct)
{
var order = [Link]([Link], [Link]);
await _repo.AddAsync(order, ct);
return [Link];
}
}
// 3. Define the Query
public record GetOrderQuery(Guid OrderId) : IRequest<OrderDto>;
// 4. Controller uses MediatR — no direct service dependency
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand cmd)
{
var orderId = await _mediator.Send(cmd);
return CreatedAtAction(nameof(Get), new { id = orderId }, null);
}
3.3 Repository Pattern — Is It Still Relevant?
Q: Should you use the Repository pattern when you already have EF Core?
Answer:
This is a deliberate trap question. The right answer acknowledges both sides. EF Core's DbContext is
already a Unit of Work, and DbSet<T> is already a Repository. Adding another repository layer can be
redundant over-engineering. However, the repository pattern still adds value when: you need to swap
data sources, write unit tests without hitting a real database, enforce query consistency, or hide
complex EF query logic from business code.
// Thin repository — wraps EF, adds testability
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken ct = default);
Task<List<Order>> GetPendingOrdersAsync(CancellationToken ct = default);
Task AddAsync(Order order, CancellationToken ct = default);
}
public class OrderRepository : IOrderRepository
{
private readonly AppDbContext _ctx;
public OrderRepository(AppDbContext ctx) => _ctx = ctx;
public Task<Order?> GetByIdAsync(Guid id, CancellationToken ct)
=> _ctx.[Link]().FirstOrDefaultAsync(o => [Link] == id, ct);
public Task<List<Order>> GetPendingOrdersAsync(CancellationToken ct)
=> _ctx.[Link](o => [Link] == [Link])
.OrderBy(o => [Link]).ToListAsync(ct);
}
// In tests: inject a mock IOrderRepository, no real DB needed
Best answer: 'I use the Repository pattern selectively. For simple CRUD, I query DbContext directly
in the handler. For complex domain queries or when I need testability without a real DB, I extract a
repository interface.'
3.4 Decorator Pattern
Q: Implement the Decorator pattern in .NET DI for cross-cutting concerns.
Answer:
The Decorator pattern adds behavior to an object without modifying it. In [Link] Core, this is perfect
for wrapping services with logging, caching, or retry logic without touching the original implementation.
public interface IProductService
{
Task<Product?> GetByIdAsync(int id);
}
// Original implementation
public class ProductService : IProductService
{
public async Task<Product?> GetByIdAsync(int id)
=> await _repo.FindAsync(id);
}
// Caching Decorator — wraps ProductService
public class CachedProductService : IProductService
{
private readonly IProductService _inner;
private readonly IMemoryCache _cache;
public CachedProductService(IProductService inner, IMemoryCache cache)
{ _inner = inner; _cache = cache; }
public async Task<Product?> GetByIdAsync(int id)
{
if (_cache.TryGetValue($"product:{id}", out Product? cached))
return cached;
var product = await _inner.GetByIdAsync(id);
_cache.Set($"product:{id}", product, [Link](10));
return product;
}
}
// Registration
[Link]<ProductService>();
[Link]<IProductService>(sp =>
new CachedProductService(
[Link]<ProductService>(),
[Link]<IMemoryCache>()));
3.5 Outbox Pattern — Reliable Messaging
Q: How do you guarantee a message is published to a queue after a DB write?
Answer:
The Outbox pattern solves the dual-write problem: saving to the database AND publishing an event
must be atomic. If the service crashes between the two, you get inconsistency. The solution: write the
event to an outbox table in the same transaction as your domain change, then have a background job
publish from the outbox.
// 1. Save order + outbox message in same DB transaction
public async Task<Guid> Handle(CreateOrderCommand cmd, CancellationToken ct)
{
var order = [Link]([Link], [Link]);
var outboxMsg = new OutboxMessage
{
Id = [Link](),
Type = "OrderCreated",
Payload = [Link](new OrderCreatedEvent([Link])),
CreatedAt = [Link]
};
_ctx.[Link](order);
_ctx.[Link](outboxMsg);
await _ctx.SaveChangesAsync(ct); // Atomic: both or neither
return [Link];
}
// 2. Background job publishes unpublished messages
public class OutboxProcessor : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
while (![Link])
{
var msgs = await _ctx.OutboxMessages
.Where(m => [Link] == null)
.Take(20).ToListAsync(ct);
foreach (var msg in msgs)
{
await _bus.PublishAsync([Link], [Link], ct);
[Link] = [Link];
}
await _ctx.SaveChangesAsync(ct);
await [Link](5000, ct); // Poll every 5 seconds
}
}
}
This directly maps to your Azure Service Bus experience at Siemens. Frame your answer as: 'At
Siemens, we faced a similar problem with reliable event publishing. Here's how we solved it...'
Section 4: Angular — Senior Level
4.1 Change Detection — The Most-Asked Angular Topic
Q: How does Angular's change detection work? What is the difference between Default and
OnPush?
Answer:
Angular's change detection is the mechanism that keeps the view in sync with the component's data. In
Default strategy, Angular checks every component in the tree from top to bottom whenever any
asynchronous event occurs (click, HTTP response, setTimeout, etc.). This is done via [Link] which
patches browser APIs to notify Angular.
In OnPush strategy, Angular only checks the component when: an @Input() reference changes, an
event originates from the component or its children, an async pipe emits a new value, or
[Link]() is explicitly called. This dramatically reduces the number of
checks in large component trees.
// Default strategy — checked on every event
@Component({ template: `<div>{{data}}</div>` })
export class DefaultComponent {
data = 'hello'; // Mutable, Angular checks every time
}
// OnPush — checked only when inputs change
@Component({
changeDetection: [Link],
template: `<div>{{[Link]}}</div>`
})
export class ProductCardComponent {
@Input() product!: Product;
// IMPORTANT: Only triggers re-render if product REFERENCE changes
// Mutating [Link] = 'New' won't trigger a re-render
// Must do: [Link] = { ...[Link], name: 'New' }
}
// Manually triggering detection with OnPush
export class DataComponent {
constructor(private cdr: ChangeDetectorRef) {}
loadData() {
this._svc.getData().subscribe(data => {
[Link] = data;
[Link](); // Tell Angular to re-check this component
});
}
}
4.2 [Link]
Q: What is [Link] and why does Angular depend on it?
Answer:
[Link] creates an execution context (a 'zone') that persists across asynchronous operations. It
monkey-patches all browser asynchronous APIs — setTimeout, setInterval, Promise, XMLHttpRequest,
addEventListener — to intercept when async tasks start and complete. Angular's NgZone uses this to
know when to trigger change detection. When any async event completes inside Angular's zone,
Angular runs change detection automatically.
// Running outside Angular's zone (no change detection triggered)
@Component({ ... })
export class TimerComponent {
constructor(private ngZone: NgZone) {}
startHeavyTimer() {
// Run outside zone — won't trigger CD on every tick
[Link](() => {
setInterval(() => {
[Link]++;
// Only update the view when needed
if ([Link] % 100 === 0) {
[Link](() => {}); // Trigger CD once
}
}, 1);
});
}
}
4.3 RxJS Operators — The Real Interview Questions
Q: What is the difference between switchMap, mergeMap, concatMap, and exhaustMap?
Answer:
All four are higher-order mapping operators — they transform an Observable of values into an
Observable of Observables and flatten them. The difference is in how they handle overlapping inner
Observables:
• switchMap: Cancels the previous inner Observable when a new emission arrives. Use for: search
autocomplete, route params. 'Switch to the latest.'
• mergeMap (flatMap): Keeps all inner Observables alive concurrently. Use for: parallel HTTP
requests where order doesn't matter.
• concatMap: Queues inner Observables, waits for each to complete before starting the next. Use
for: sequential operations where order matters (e.g., upload files one by one).
• exhaustMap: Ignores new emissions while an inner Observable is active. Use for: login buttons
(ignore double-clicks while request is in flight).
// switchMap — search: cancel previous request on new keystroke
[Link](
debounceTime(300),
distinctUntilChanged(),
switchMap(term => [Link](term)) // Cancels previous
).subscribe(results => [Link] = results);
// mergeMap — parallel uploads
from(filesToUpload).pipe(
mergeMap(file => [Link](file)) // All concurrent
).subscribe();
// concatMap — sequential, ordered processing
from(orderedTasks).pipe(
concatMap(task => [Link](task)) // One at a time, in order
).subscribe();
// exhaustMap — login button: ignore clicks while logging in
[Link](
exhaustMap(() => [Link](credentials)) // Ignores mid-flight
).subscribe();
4.4 Memory Leaks in Angular
Q: How do you prevent memory leaks from RxJS subscriptions in Angular?
Answer:
Every subscribe() creates a subscription that holds a reference. If you don't unsubscribe when the
component is destroyed, the subscription stays alive and the callback keeps running — even after the
component is gone. There are four clean patterns to avoid this:
// BAD — subscription never cleaned up
export class BadComponent implements OnInit {
ngOnInit() {
[Link]().subscribe(d => [Link] = d); // LEAK
}
}
// BEST — async pipe (auto-unsubscribes)
@Component({ template: `<div *ngFor="let item of items$ | async">{{item}}</div>` })
export class GoodComponent {
items$ = [Link](); // No subscribe, no leak
}
// GOOD — takeUntilDestroyed (Angular 16+, the modern way)
export class ModernComponent {
private destroyRef = inject(DestroyRef);
ngOnInit() {
[Link]()
.pipe(takeUntilDestroyed([Link])) // Auto-unsubscribes
.subscribe(d => [Link] = d);
}
}
// GOOD — Subject + takeUntil (pre-Angular 16)
export class OldComponent implements OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
[Link]()
.pipe(takeUntil([Link]$))
.subscribe(d => [Link] = d);
}
ngOnDestroy() { [Link]$.next(); [Link]$.complete(); }
}
4.5 Subject Types
Q: What is the difference between Subject, BehaviorSubject, and ReplaySubject?
Answer:
• Subject: No initial value. New subscribers only get future emissions. Like a radio — if you tune in
after the song started, you missed it.
• BehaviorSubject: Requires an initial value. New subscribers immediately get the current/latest
value. Use for: component state, selected item, current user.
• ReplaySubject(n): Replays the last n emissions to new subscribers. Use for: event history, action
logs, undoable state.
// BehaviorSubject — most common in Angular apps
@Injectable({ providedIn: 'root' })
export class CartService {
private cartItems$ = new BehaviorSubject<CartItem[]>([]); // Initial: empty
// Expose as Observable — consumers can't call .next()
items$ = [Link]$.asObservable();
addItem(item: CartItem) {
const current = [Link]$.getValue();
[Link]$.next([...current, item]);
}
}
// Consumer
@Component({ template: `Cart: {{cartCount$ | async}} items` })
export class HeaderComponent {
cartCount$ = [Link]$.pipe(map(items => [Link]));
}
4.6 Angular Performance Optimization
Q: How do you optimize a slow Angular application?
Answer:
Approach this systematically — profile first, optimize second:
• 1. OnPush: Use OnPush change detection everywhere. This alone can reduce detection cycles
by 80% in component-heavy apps.
• 2. trackBy: Use trackBy with *ngFor. Without it, Angular destroys and recreates every DOM
element on array changes. TrackBy returns a unique identifier so Angular reuses existing
elements.
• 3. Async pipe: Use the async pipe instead of manual subscriptions. It auto-subscribes and auto-
unsubscribes, and triggers change detection correctly with OnPush.
• 4. Lazy loading: Lazy load feature modules. Don't load all routes upfront. Use loadChildren: () =>
import('./orders/[Link]') to split the bundle.
• 5. Pure pipes: Use pure pipes for expensive transformations instead of methods in the template.
Template methods run on every change detection cycle. Pure pipes only run when input changes.
// trackBy prevents full list re-render
@Component({
template: `
<div *ngFor="let order of orders; trackBy: trackById">
<app-order-card [order]="order" />
</div>`
})
export class OrderListComponent {
trackById = (index: number, order: Order) => [Link];
}
// Pure pipe — runs only when input reference changes
@Pipe({ name: 'formatCurrency', pure: true })
export class FormatCurrencyPipe implements PipeTransform {
transform(value: number, currency: string): string {
return new [Link]('en-IN', {
style: 'currency', currency
}).format(value);
}
}
Section 5: TypeScript — Advanced Patterns
5.1 Utility Types
Q: Explain and use Partial, Required, Pick, Omit, and Record.
Answer:
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// Partial — all properties optional (use for update DTOs)
type UpdateUserDto = Partial<User>; // { id?: number; name?: string; ... }
// Required — all properties required (opposite of Partial)
type CompleteUser = Required<Partial<User>>;
// Pick — select specific properties
type UserPreview = Pick<User, 'id' | 'name'>; // { id: number; name: string }
// Omit — exclude specific properties (use for create DTOs)
type CreateUserDto = Omit<User, 'id'>; // No id — DB generates it
// Record — map of keys to values
type RolePermissions = Record<'admin' | 'user', string[]>;
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read']
};
5.2 Discriminated Unions
Q: What is a discriminated union and where is it useful?
Answer:
A discriminated union is a union type where each member has a common literal property (the
discriminant) that TypeScript uses to narrow the type in a switch or if statement. It's perfect for
modeling state machines, API responses, and event systems.
// Discriminated union for API state
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function renderState<T>(state: ApiState<T>) {
switch ([Link]) {
case 'idle': return '<div>Start search</div>';
case 'loading': return '<div>Loading...</div>';
case 'success': return `<div>${[Link]([Link])}</div>`;
case 'error': return `<div>Error: ${[Link]}</div>`;
// TypeScript ensures exhaustive matching
}
}
// In Angular component
state: ApiState<Product[]> = { status: 'idle' };
loadProducts() {
[Link] = { status: 'loading' };
[Link]().subscribe({
next: data => [Link] = { status: 'success', data },
error: err => [Link] = { status: 'error', error: [Link] }
});
}
5.3 Generics with Constraints
// Generic repository interface
interface Repository<T extends { id: string | number }> {
findById(id: T['id']): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: T['id']): Promise<void>;
}
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
meta: { total: number; page: number; size: number };
errors: string[];
}
// Generic service method
async function fetchPaginated<T>(
url: string,
params: Record<string, unknown>
): Promise<ApiResponse<T>> {
const response = await fetch(`${url}?${new URLSearchParams(params as any)}`);
return [Link]() as Promise<ApiResponse<T>>;
}
// Usage
const orders = await fetchPaginated<Order>('/api/orders', { page: 1, size: 20 });
Section 6: REST API Design & Security
6.1 RESTful API Best Practices
Q: What makes a good REST API design? Walk me through your approach.
Answer:
• Use nouns for resources, not verbs: /orders not /getOrders
• HTTP verbs carry the intent: GET (read), POST (create), PUT (full update), PATCH (partial
update), DELETE (remove)
• Return proper HTTP status codes: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401
Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity, 500 Server
Error
• Use plural nouns: /products, /orders, /customers
• Nest resources for relationships: GET /customers/{id}/orders
• Use query params for filtering, sorting, pagination: GET /orders?
status=pending&page=1&size=20&sort=createdAt:desc
• Version your API: /api/v1/orders or Accept: application/[Link]+json;version=2
// Good API design example
GET /api/v1/orders // List with filters
POST /api/v1/orders // Create
GET /api/v1/orders/{id} // Get one
PUT /api/v1/orders/{id} // Full replace
PATCH /api/v1/orders/{id}/status // Partial update
DELETE /api/v1/orders/{id} // Delete
GET /api/v1/orders/{id}/items // Nested resource
// Response envelope — consistent structure
{
"data": { "id": 1, "status": "pending" },
"meta": { "requestId": "abc-123", "timestamp": "2025-01-01T00:00:00Z" }
}
// Error response — RFC 7807 ProblemDetails
{
"type": "[Link]
"title": "Validation failed",
"status": 400,
"detail": "Order quantity must be greater than 0",
"errors": { "quantity": ["Must be > 0"] }
}
6.2 JWT Authentication
Q: How does JWT authentication work end-to-end in a .NET + Angular app?
Answer:
JWT (JSON Web Token) is a stateless authentication mechanism. The server issues a signed token
containing claims (user ID, roles, expiry). The client sends it on every request. The server validates the
signature — no database lookup needed.
// .NET — JWT configuration
[Link]([Link])
.AddJwtBearer(options => {
[Link] = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = config["Jwt:Issuer"],
ValidAudience = config["Jwt:Audience"],
IssuerSigningKey = new SymmetricSecurityKey(
[Link](config["Jwt:Key"]!))
};
});
// Token generation
public string GenerateToken(User user)
{
var claims = new[]
{
new Claim([Link], [Link]()),
new Claim([Link], [Link]),
new Claim([Link], [Link])
};
var key = new SymmetricSecurityKey([Link](_config["Jwt:Key"]!));
var token = new JwtSecurityToken(
issuer: _config["Jwt:Issuer"],
audience: _config["Jwt:Audience"],
claims: claims,
expires: [Link](1),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
return new JwtSecurityTokenHandler().WriteToken(token);
}
// Angular — HTTP Interceptor to attach token
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
constructor(private authService: AuthService) {}
intercept(req: HttpRequest<unknown>, next: HttpHandler) {
const token = [Link]();
if (!token) return [Link](req);
const authReq = [Link]({
headers: [Link]('Authorization', `Bearer ${token}`)
});
return [Link](authReq).pipe(
catchError(err => {
if ([Link] === 401) [Link]();
return throwError(() => err);
})
);
}
}
// Angular — Route Guard
@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {
constructor(private auth: AuthService, private router: Router) {}
canActivate(route: ActivatedRouteSnapshot): boolean {
if (![Link]()) {
[Link](['/login']);
return false;
}
const requiredRole = [Link]['role'];
if (requiredRole && ) {
[Link](['/forbidden']);
return false;
}
return true;
}
}
Store JWT in memory (a variable) or httpOnly cookies. NEVER in localStorage — vulnerable to
XSS. Use a refresh token (stored in httpOnly cookie) to get new access tokens silently.
Section 7: System Design Essentials
7.1 Microservices Patterns
Q: What patterns do you use to handle failures in a microservices architecture?
Answer:
• Circuit Breaker: Detect repeated failures and 'open the circuit' to stop calling a failing service.
After a timeout, allow a single test request. In .NET, use Polly. Prevents cascading failures.
• Retry with Exponential Backoff: Retry failed requests with increasing delays. Add jitter
(randomness) to avoid thundering herd when many services retry simultaneously.
• Bulkhead: Isolate failures. Use separate thread pools or connection limits per downstream service
so one slow service doesn't exhaust resources for all.
• Timeout: Never wait indefinitely. Set strict timeouts on all external calls. Fail fast is better than
hanging.
// Polly — Circuit Breaker + Retry in .NET
[Link]<IOrderClient, OrderClient>()
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<HttpRequestException>()
.OrResult(r => ![Link])
.WaitAndRetryAsync(3, attempt =>
[Link]([Link](2, attempt)) // 2s, 4s, 8s
+ [Link]([Link](0, 500)))) // Jitter
.AddPolicyHandler(Policy<HttpResponseMessage>
.Handle<Exception>()
.CircuitBreakerAsync(
exceptionsAllowedBeforeBreaking: 5,
durationOfBreak: [Link](30)));
7.2 Caching Strategy
Q: What is your caching strategy for a high-read API?
Answer:
• Cache-aside (Lazy): Check cache first. On miss, fetch from DB, store in cache, return. Best for
read-heavy, write-occasional data.
• Write-through: On write, update both cache and DB simultaneously. Cache is always consistent
but adds write latency.
• TTL-based eviction: Set expiry based on how stale the data can tolerate (products: 10 min,
exchange rates: 30 sec, user profile: 5 min).
• Cache invalidation: When data changes, explicitly remove/update the cache entry. Hardest part of
caching.
// Cache-aside with IMemoryCache
public async Task<Product?> GetProductAsync(int id)
{
var key = $"product:{id}";
if (_cache.TryGetValue(key, out Product? cached))
return cached; // Cache hit
// Cache miss — fetch from DB
var product = await _repo.GetByIdAsync(id);
if (product is not null)
_cache.Set(key, product, new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = [Link](10),
SlidingExpiration = [Link](2)
});
return product;
}
// Distributed cache with Redis (for multi-instance deployments)
public async Task<Product?> GetProductAsync(int id)
{
var key = $"product:{id}";
var cached = await _redis.GetStringAsync(key);
if (cached is not null)
return [Link]<Product>(cached);
var product = await _repo.GetByIdAsync(id);
if (product is not null)
await _redis.SetStringAsync(key,
[Link](product),
new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow =
[Link](10) });
return product;
}
7.3 SQL Optimization
Q: How do you optimize slow database queries?
Answer:
This maps directly to your resume. Walk through it systematically:
• Identify: Use execution plans (SQL Server Management Studio or Azure Data Studio), Application
Insights slow query logs, or SET STATISTICS IO ON.
• Indexes: Add indexes on columns used in WHERE, JOIN, and ORDER BY. Covering indexes
include all selected columns, avoiding a key lookup.
• Avoid SELECT *: Select only needed columns. Reduces IO and network transfer.
• Avoid N+1: Use JOINs or subqueries instead of looping queries.
• Stored procedures: Pre-compiled, cached execution plans. Your 60% improvement at Siemens
likely combined indexing + stored procedures.
• Read replicas: Route read queries to a read replica, freeing the primary for writes.
-- Before optimization (slow)
SELECT * FROM Orders o
JOIN Customers c ON [Link] = [Link]
WHERE [Link] = 'Pending' AND [Link] > '2024-01-01'
ORDER BY [Link] DESC
-- After optimization
-- 1. Create composite index
CREATE INDEX IX_Orders_Status_CreatedAt
ON Orders (Status, CreatedAt DESC)
INCLUDE (Id, CustomerId, TotalAmount); -- Covering index
-- 2. Select only needed columns
SELECT [Link], [Link], [Link], [Link]
FROM Orders o
JOIN Customers c ON [Link] = [Link]
WHERE [Link] = 'Pending' AND [Link] > '2024-01-01'
ORDER BY [Link] DESC
Section 8: Behavioural Questions — STAR Answers
Your two killer stories: (1) LLM-to-SQL feature at Siemens — innovation. (2) 60% query speed
improvement — impact. Lead with these when possible.
Q: Tell me about a technically complex problem you solved.
Answer:
Situation: At Siemens Healthineers, business users needed to search medical instrument data using
natural language — they couldn't write SQL, and the existing keyword search was too rigid.
Task: Build a system that translates natural language questions into valid SQL queries against our
Azure SQL schema.
Action: I researched LLM-based text-to-SQL approaches, designed a prompt engineering layer that
included schema context and sample rows, built an [Link] Core service that called the LLM API with
the user's query, validated the generated SQL against a whitelist of allowed tables, executed it against
Azure SQL, and returned structured results. I also built a feedback loop so invalid queries were logged
and improved the prompt.
Result: Business users could now ask 'Show me all IB Search Engine devices serviced in Q3 with error
code 0x44' in plain English. This reduced analyst dependency on the dev team for data queries
significantly.
Q: Describe a time you significantly improved performance.
Answer:
Situation: Our IB Search Engine backend was experiencing slow query response times — averaging
over 3 seconds for complex instrument lookups, causing user frustration.
Task: Identify bottlenecks and optimize without breaking existing functionality.
Action: I used Azure Application Insights to identify the slowest queries. I found three issues: missing
composite indexes on frequently filtered columns, stored procedures doing sequential table scans, and
N+1 patterns in the EF Core queries feeding the API. I added covering indexes, rewrote critical stored
procedures with proper CTEs, and refactored EF Core queries to use projection (Select with DTO)
instead of loading full entities.
Result: Average query time dropped from 3.2 seconds to 1.3 seconds — a 60% improvement. This was
measured and validated in Application Insights dashboards.
Q: Why are you leaving Siemens Healthineers?
Answer:
I've had an excellent experience at Siemens — worked on meaningful healthcare technology, led
impactful features, and grown as a full-stack engineer. I'm leaving because I want to work in a product-
first engineering culture where I can have broader ownership, closer proximity to business impact, and
opportunity to work with modern tooling and architecture at a faster iteration pace. I'm specifically
excited about companies where engineering decisions directly influence the product roadmap.
Section 9: Quick Reference — Interview Cheatsheet
.NET / C# — Top Questions at 3.5 Years
• async/await state machine — what does the compiler generate?
• ConfigureAwait(false) — when and why?
• Task vs ValueTask — which for hot paths?
• GC generations — Gen0, Gen1, Gen2, LOH
• Captive dependency — Singleton injecting Scoped
• IDisposable vs Finalizer — when each?
• Record vs class — value equality, immutability
• CQRS with MediatR — command/handler/query
• Outbox pattern — reliable event publishing
• N+1 in EF Core — how to detect and fix
• AsNoTracking() — read-only queries
• Decorator pattern in DI — wrapping services
• Circuit breaker with Polly — failure handling
• Middleware order — authentication before authorization
• Global error handling — IExceptionHandler
Angular — Top Questions at 3.5 Years
• Change detection: Default vs OnPush — draw the tree
• [Link] — what it patches and why Angular needs it
• switchMap vs mergeMap vs concatMap vs exhaustMap
• Memory leaks — takeUntilDestroyed vs async pipe
• BehaviorSubject vs Subject — initial value difference
• trackBy in *ngFor — DOM reuse
• Lazy loading — loadChildren route config
• Pure vs impure pipes — when each runs
• JWT interceptor — attaching Bearer token
• Auth guard — route protection with roles
• OnPush + markForCheck vs detectChanges
• Standalone components vs NgModules
• Angular Signals — new reactivity model
TypeScript — Quick Wins
• Utility types: Partial, Required, Pick, Omit, Record — usage examples
• Discriminated unions — type narrowing via switch
• Generic constraints — where T extends ...
• Mapped types — transforming interface properties
• never type — exhaustive type checking
Questions to Ask the Interviewer
• What does the engineering team's on-call rotation look like?
• How are technical decisions made — top-down or team consensus?
• What's the biggest technical challenge the team is working through right now?
• How does the team approach technical debt?
• What does a successful first 90 days look like in this role?
• What's the deployment pipeline — how often do you ship to production?
Good luck, Ujjwal. Go all in.