cswork / libraryOpen practice workspace

Interview guides · Agilité Solutions

Agilité Solutions Software Engineer Interview Guide

Agilité Solutions Software Engineer interview guide: four reported rounds, ASP.NET Core, Angular and Entity Framework questions, and a 7-day plan.

The 60-second brief

Candidates describe the Software Engineer role as building, maintaining and scaling web applications that support the company's business operations. The reported stack is an ASP.NET Core Web API back end, an Angular front end and Entity Framework for data access, so the work is described as full stack. The responsibilities candidates list are feature development inside existing software, refactoring code to production standard, investigating bugs and performance bottlenecks, and giving stakeholders progress updates and clarifying requirements. No product names or customers are reported, so this guide prepares you for the stack and the engineering habits rather than for a specific product.

This guide is for engineers interviewing for the Software Engineer role at Agilité Solutions, especially those with .NET and Angular experience.

Candidates report four rounds over roughly 3-5 weeks: Initial Screening, Hands-on Technical Validation, Behavioral Assessments and a Final HR Discussion. An initial assignment is also reported, and later technical conversation is said to build on it.

Focus areas

  • .NET / ASP.NET Core Web API
  • Angular
  • Entity Framework

Overview

The reported stack tells you where to spend your hours. A request in this role travels from an Angular component through a service call to an ASP.NET Core Web API controller, into Entity Framework, and back as a response the front end has to handle. Prepare one feature you can narrate along that whole path: the DTO shape, validation, the query that loads the data, the error response when something fails, and how the Angular side shows that error. Being able to describe a single vertical slice in detail is worth more than a shallow tour of every framework feature.

The reported questions split into clear groups. Three are framework-concept questions: the difference between lazy loading and eager loading, the difference between an abstract class and an interface and when to prefer each, and how to handle concurrency in Entity Framework. One is a hands-on implementation using ASP.NET Core Web API and Angular. One is a coding task: write your own version of the array reduce method. The concept questions reward answers that name the mechanism (Include, navigation properties, a row version column, DbUpdateConcurrencyException) and then give a trade-off, so rehearse each as a short spoken answer with one concrete example from code you have written.

Candidates also report an emphasis on production-ready code and on explaining the reasons behind technical choices. For preparation, that means treating every practice exercise as if it were going to be deployed: validate input, return meaningful errors, name things clearly, write at least one test, and be able to say why you picked a library or pattern over its alternatives. Since an initial assignment is reported and later discussion is said to build on it, keep your own decisions in it easy to defend: you should be able to explain what you would refactor for scale and what you left out on purpose.

The Agilité Solutions interview process

Candidates report four rounds that move from screening to hands-on technical validation, then behavioral assessments, ending with an HR discussion about the offer and next steps. The reported span is about 3-5 weeks.

Initial Screening

Candidates describe this first stage as a screening to check fit for the role. The reported material does not list questions for it, so treat it as the place where your .NET and Angular background is summarised and the logistics of the rest of the loop are confirmed. Candidates also report that prompt, professional communication with the recruitment contact matters throughout, so this is where that starts.

What to demonstrate

  • Fit for the role as candidates describe it: full-stack work across ASP.NET Core Web API and Angular
  • How clearly you summarise your own experience with Entity Framework and production code
  • Responsiveness and professionalism in how you communicate

How to prepare

  • Write a two-minute account of your most relevant project that names the Web API, the Angular piece and the data access layer you used
  • Prepare a one-line answer for each reported stack item: ASP.NET Core Web API, Angular, Entity Framework
  • Ask what the initial assignment involves and how its results are used in later rounds

Hands-on Technical Validation

Candidates describe this as a technical validation of coding skills and problem solving. A hands-on stage is where the reported coding and implementation questions are practised: building something with ASP.NET Core Web API and Angular, and writing a clone of the reduce array method. The framework-concept questions on lazy versus eager loading, abstract class versus interface, and Entity Framework concurrency belong to the same technical preparation. Candidates are advised to talk through their reasoning while coding.

What to demonstrate

  • Working code for the hands-on implementation and the reduce clone
  • Problem solving explained out loud as you go
  • Depth in .NET, Entity Framework and Angular, including why you chose a pattern or library
  • Production-readiness: error handling, naming and testing

How to prepare

  • Build a small endpoint plus Angular screen end to end under a time limit, with a DTO, validation, an error response and one test on each side
  • Implement your own reduce in TypeScript and test it with and without an initial value, on an empty array, and with the index and array arguments
  • Rehearse spoken answers on lazy versus eager loading and on Entity Framework concurrency, each with a short code example
  • Practise narrating decisions while you type, since the reported advice is to explain your reasoning as you work

Behavioral Assessments

Candidates describe this stage as behavioral assessments focused on team compatibility and cultural fit. The role description candidates report stresses articulating technical concepts, professional accountability and keeping stakeholders informed, so the stories that fit best are about technical decisions you owned, requirements you had to clarify, and work you took from prototype to production.

What to demonstrate

  • Team compatibility and cultural fit, as candidates describe the stage
  • How you explain technical decisions to non-specialists and teammates
  • Accountability for outcomes and how you update stakeholders

How to prepare

  • Write three stories with a situation, your decision, the alternative you rejected and the result, using work on APIs, Angular screens or data access
  • Prepare a story about choosing between an interface and an abstract class, and one about speeding up a slow endpoint, since both appear as reported example scenarios
  • Prepare one story where you clarified unclear requirements with a stakeholder before building

Final HR Discussion

Candidates describe the final stage as a discussion with HR about the offer and next steps. Candidates also report that professional maturity, communication and alignment with the company's culture come up in the closing stages. Preparation here is practical rather than technical.

What to demonstrate

  • Professional maturity and clear communication
  • Alignment with the company, as candidates describe it
  • Readiness to discuss offer details and next steps

How to prepare

  • Write down your notice period, location constraints and compensation expectations so you can state them plainly
  • Prepare two or three questions about the team's day-to-day work with ASP.NET Core and Angular
  • Decide in advance how you will keep in touch with your recruiter while decisions are pending

Where candidates lose points

Defining lazy loading and eager loading without saying what each does to the SQL that runs

Tie each term to the queries it produces. Show a loop over parent rows that triggers one extra query per row (the N+1 pattern), then fix it with Include and ThenInclude, a projection with Select, or a split query. Turn on EF query logging in practice so you can describe the actual statements, and mention explicit loading as the third option.

Answering abstract class versus interface as a list of syntax differences

Give the deciding trade-offs: a class can inherit one base but implement many interfaces, an abstract class can carry shared state and implementation, and interfaces suit dependency injection and mocking in tests. Close with a concrete case from your own code, because choosing between the two is also a reported example scenario for the technical rounds.

Saying concurrency in Entity Framework is handled with transactions or locks, with no mention of a concurrency token or DbUpdateConcurrencyException

Lead with optimistic concurrency, a rowversion column and the exception, then say what the API returns and what the user sees. Add the pessimistic option as a contrast, with its cost, rather than as the whole answer.

Writing a reduce clone that only handles the happy path

Cover the no-initial-value case (the first element becomes the accumulator and iteration starts at index 1), an empty array with no initial value (the built-in throws a TypeError), the callback receiving accumulator, current value, index and array, and skipping holes in sparse arrays. Do not mutate the input, and write the tests as you go.

Handing in working code that is not production-ready, or being unable to justify choices in it

Before submitting any assignment or exercise, check for input validation, consistent error responses, clear names, at least one test per layer, and a short README of decisions and known limits. Keep a one-sentence reason for every library and pattern you used, plus the alternative you rejected, since candidates report that follow-up questions go to those reasons.

Questions & practice

Create a clone of the reduce array method.

Medium · Problem Solving and Logic

Approach

  1. Restate the contract before writing code. The callback receives (accumulator, currentValue, index, array). With an initial value, iteration starts at index 0. Without one, the first present element becomes the accumulator and iteration starts after it. An empty array with no initial value throws a TypeError, and so does a callback that is not a function.
  2. Detect a missing initial value with arguments.length (or a rest parameter's length), never with initialValue === undefined, because undefined is a legal seed. Write it as a standalone function, or define it with Object.defineProperty as non-enumerable if it must sit on Array.prototype. Overwriting a built-in is a choice you should be ready to defend.
  3. Loop over indices from 0 to a length captured once at the start. Skip holes with i in arr, the way the built-in does for sparse arrays. Pass the original array as the fourth argument. That gives O(n) time and O(1) extra space beyond whatever the callback builds.
  4. Test the edges aloud: a sum with and without a seed, a single-element array with no seed (returns that element without calling the callback), an empty array with a seed (returns the seed), an empty array without one (throws), and a sparse array.
  5. If the screen is in C#, the equivalent is LINQ Aggregate. The overload without a seed throws InvalidOperationException on an empty sequence, and the seeded overload returns the seed. A generic signature such as reduce<T, U>(arr: T[], fn: (acc: U, cur: T, i: number, a: T[]) => U, init: U): U shows type thinking in TypeScript.

Follow-up

  • Implement map and filter using only your reduce.
  • How would you write reduceRight, or a version that awaits an async callback in sequence?
  • What happens if the callback pushes to or deletes from the array during iteration?

Find overlapping job attempts and peak concurrency from lease records

Medium · sweep line · intervals · leases

A day of job_run history yields about 50,000,000 attempt records: (job_run_id, job_type, attempt, started_at, finished_at which is NULL when the worker died, lease_expires_at). Leases expire on a clock, so a job that outran its lease ran twice. Produce (a) every job_run_id whose attempts overlapped in wall-clock time and (b) the peak number of simultaneously running attempts per job_type with the minute it occurred. Target O(n log n). State how you treat a NULL finished_at and what clock skew does to your answer.

Approach

  1. Define the interval before sorting anything: an attempt occupies [started_at, COALESCE(finished_at, lease_expires_at)). finished_at is observed and lease_expires_at is only a promise, so every attempt without a finish contributes an estimate and the whole result is a lower bound on overlap rather than an exact count.
  2. For peak concurrency, sweep: emit 2n endpoints, sort by (timestamp, kind) with ends ordered before starts at equal timestamps, then walk the sequence maintaining a counter per job_type and record each type's maximum with its timestamp. O(n log n) dominated by the sort, O(n) space, or O(1) extra if the sort is external and the walk streams.
  3. For overlap detection, do not compare attempts pairwise. A single global sort by (job_run_id, started_at) gives both the grouping and the order; within a group, keep the maximum end seen so far and report an overlap exactly when the next start is less than that running maximum, which is one linear pass after the sort.
  4. Half-open intervals matter and are easy to get wrong: with closed intervals an attempt ending at the same millisecond another begins reads as concurrency two, and across 50,000,000 records that artefact swamps the real signal.

Follow-up

  • A handler is not idempotent and you have found 400 overlapping jobs. Which of them actually caused damage, and what would you query to find out?
  • Peak concurrency for one job_type is 4 against a configured cap of 4. Is the cap working, or is the data hiding attempts that never started?

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Replace offset paging on the resource feed with keyset

Easy · keyset pagination · composite index · sort order

resource holds resource_id, tenant_id, owner_user_id, title, body_ref, version, status ('draft','active','archived','deleted'), created_at, updated_at, deleted_at, with an index on (tenant_id, status, updated_at DESC, resource_id DESC). The listing endpoint returns active resources for one tenant, newest update first, 50 per page, today with LIMIT 50 OFFSET n. Tenants reach page 400 and rows are created while they read. Write the keyset query, define what the cursor carries and how it is encoded, and say which part of the index each predicate uses. Assume PostgreSQL 16.

Approach

  1. Name the two failures separately. OFFSET 20000 makes the server produce and discard 20,000 rows, so page cost grows with depth rather than with page size. Independently, any write that changes how many rows sort above the offset moves the window between two fetches, and the direction decides which anomaly you get: an insert lands at the head of updated_at DESC and pushes already-returned rows down past the boundary, so they are returned a second time; a delete above the offset, or a row whose updated_at is bumped above the cursor, pulls rows up and one is never returned at all. Nothing in the response reveals either.
  2. Write the seek: WHERE tenant_id = $1 AND status = 'active' AND (updated_at, resource_id) < ($2, $3) ORDER BY updated_at DESC, resource_id DESC LIMIT 50. The row-value comparison is one index range rather than a disjunction, and both columns are NOT NULL, which is what makes that comparison well defined.
  3. Map each predicate onto the index: tenant_id and status are equality on the leading columns, (updated_at, resource_id) is the range, and the ORDER BY matches the index order so no Sort node appears and the scan stops after 50 rows. The DESC in the definition only matters for mixed directions — a plain ascending btree on the same columns is read backwards for this query.
  4. Put both sort columns in the cursor and nothing the client can tamper with into another tenant: base64 of (updated_at, resource_id), validated server-side, with tenant_id taken from the principal.

Follow-up

  • The client asks for 'jump to page 400'. What do you offer instead, and what does the honest version cost?
  • Sort order becomes user-selectable across four columns. How many indexes is that, and which would you refuse to add?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

Difference between Lazy Loading and Eager Loading.

Medium · Technical Foundations and Frameworks

Approach

  1. Start by asking which layer is meant. In Entity Framework it is about when related entities load. In Angular it is about when route bundles download: loadChildren or loadComponent load on navigation, while eagerly imported routes ship in the initial bundle. Answer the EF meaning first, since data access is the reported topic, and offer the Angular one afterwards.
  2. Eager loading means Include and ThenInclude: one query (or one per collection with AsSplitQuery) fetches the root and the navigations you name, before you use them. Lazy loading in EF Core is opt-in. It needs UseLazyLoadingProxies from the Proxies package plus virtual navigation properties, or an injected ILazyLoader, and it queries the first time a navigation is touched.
  3. Name the trade-off. Lazy loading in a loop gives N+1 queries: 1 for the list and 1 per row. Eager loading of several collections in a single query can return a Cartesian product, and AsSplitQuery fixes that at the cost of extra round trips and no single-snapshot consistency. Explicit loading (Entry(x).Collection(...).Load()) sits in between.
  4. For a Web API endpoint, my default is to project with Select into a DTO and add AsNoTracking. That loads exactly the columns the response needs, needs neither Include nor proxies, and avoids the serializer walking lazy navigations or reference cycles.
  5. Edge cases: touching a lazy navigation after the DbContext is disposed fails at runtime. Filtered Include (EF Core 5+) narrows which children load. To confirm the query count, I check the SQL EF generates through LogTo or the logging output.

Follow-up

  • An endpoint returns 100 orders and the log shows 101 queries. How do you find and fix the cause?
  • When would you choose AsSplitQuery over a single query, and what does it cost you?
  • How would you lazy-load a feature route in Angular and preload it in the background?

Difference between an abstract class and an interface, and when to prefer one over the other.

Medium · Technical Foundations and Frameworks

Approach

  1. Give the mechanical differences first. A C# class inherits from one base class but can implement many interfaces. An abstract class can hold instance fields, constructors, protected members and partial implementations. An interface cannot hold instance state. Structs can implement interfaces but cannot inherit from an abstract class.
  2. Account for modern C#. Since C# 8, interfaces can carry default method implementations. Since C# 11 they can declare static abstract members, which generic math uses. The remaining difference is about state and identity, not just whether method bodies are allowed.
  3. Rule of thumb: use an interface for a capability or contract that unrelated types can share and that DI or a test double substitutes, such as IOrderRepository registered in ASP.NET Core DI and mocked in unit tests. Use an abstract class when subclasses share real state and a fixed algorithm, as in the template-method pattern (a base class runs the steps and calls abstract hooks).
  4. Versioning trade-off: adding a member to a published interface breaks every implementer unless it has a default implementation. Adding a concrete virtual method to an abstract base does not, but it ties every subclass to that base for good.
  5. Back it with an example from your own code: why you chose one over the other, and what you would change. Two often-requested follow-ups are explicit interface implementation and the choice between an abstract base and composition.

Follow-up

  • If interfaces can have default methods now, what can an abstract class still do that an interface cannot?
  • How does ASP.NET Core dependency injection use interfaces, and what do service lifetimes have to do with it?
  • When would you choose composition over both?

Hands-on implementation using ASP.NET Core Web API and Angular.

Medium · Technical Foundations and Frameworks

Approach

  1. Clarify scope first: one resource or several, whether authentication is required, the expected data size, and whether persistence is EF Core. Then build the thinnest vertical slice end to end, from database through controller to Angular component, before polishing any one layer.
  2. Back end: an [ApiController] controller (or minimal API) taking request and response DTOs, never the EF entities themselves. A scoped DbContext through DI. Async queries with AsNoTracking and Select projection for reads. Pagination on list endpoints. Correct status codes: 201 with a Location header on create, 404 when missing, and validation errors as 400 ProblemDetails, which [ApiController] returns automatically on invalid model state.
  3. Front end: a typed Angular service that wraps HttpClient and returns Observables of interfaces that mirror the DTOs. A component that renders with the async pipe, or unsubscribes on destroy. Reactive forms with validators that match the server's rules. An HttpInterceptor for auth headers and shared error handling. In development, either a CORS policy on the API or proxy.conf.json.
  4. Production readiness you can point to: global exception handling middleware, logging, configuration through appsettings and environment variables, and tests. On the server, use xUnit, with WebApplicationFactory for integration tests. On the client, use TestBed with HttpTestingController.
  5. Be ready to explain every library and pattern in the solution and the alternative you rejected. Also say what you would change at ten times the load: caching, indexes on the filtered columns, and moving from offset to keyset pagination.

Follow-up

  • One of your endpoints is slow in production. Walk through how you would find and fix the cause.
  • Two users edit the same record at once. What does your API return, and how does the Angular form handle it?
  • How do you keep the DTO contract between the C# API and the TypeScript models from drifting?

One log partition stops advancing while the others drain

Easy · poison message · head-of-line blocking · consumer lag

Search results for a subset of tenants are hours stale; the rest are current. The projection consumer reports lag of zero on 15 of 16 partitions and 400,000 on one. Its error rate is flat and its CPU is idle. outbox_event has no pending rows older than a second, so the relay has published everything it holds. Identify the mechanism, give the ordered checks, and state what you do in the first ten minutes versus what you change permanently.

Approach

  1. Read the lag distribution first. A slow consumer lags everywhere; zero on fifteen partitions and 400,000 on one is not throughput. Idle CPU on the stuck partition means the consumer is not advancing its offset at all, which points at one message it cannot get past rather than at a rate problem.
  2. Exonerate the producer before touching the consumer. No pending outbox rows older than a second means the relay published, so the event exists in the log. This separates never sent from sent and never applied, which are different code paths and usually different owners.
  3. Read the message at the stuck offset and the handler's log lines for its event_id. A flat error rate with no progress has two explanations and you must distinguish them: the handler is throwing and the retry loop is swallowing it, or the handler is blocking on something and never returning. Idle CPU with no error lines favours the second.
  4. Mitigate before diagnosing further. Move the offending event to a dead-letter store and commit the offset past it. Adding consumers does nothing here, because a partition is consumed by exactly one member of the group, and the blast radius is every aggregate hashed to that partition, not only the aggregate that produced the bad event.

Follow-up

  • The dead-lettered event carried aggregate_version 7 and the projection had applied 6. What must the replay do differently if 8 and 9 landed in the meantime?
  • How do you show staleness to the user while the partition is behind, given the API already returns the projection's watermark?

Suggested practice checklist

The plan follows the reported loop: Entity Framework and concurrency first, then the language and coding questions, then a full Web API plus Angular build, behavioral stories, and a closing dry run.

Day 1 — Map the loop and set up a vertical slice

  • Write the four reported rounds in order and note what you will need for each; mark the one you are least ready for
  • Create a small ASP.NET Core Web API project with one entity, an Entity Framework context and a controller, and an Angular app with one service and one component that call it
  • Write down the five reported questions and rate your confidence on each from 1 to 5

Deliverable: A one-page loop map, a running API plus Angular slice, and a ranked list of the five reported questions.

Day 2 — Entity Framework: lazy versus eager loading

  • Add a parent-child relationship to your slice and turn on query logging
  • Reproduce an N+1 pattern with lazy loading, then fix it with Include, then with a Select projection, and compare the SQL each produces
  • Speak a two-minute answer on lazy versus eager loading, including explicit loading and when each is the right call

Deliverable: A before-and-after query log showing the N+1 fix, plus a recorded spoken answer.

Day 3 — Entity Framework: concurrency

  • Add a rowversion column to the entity and trigger DbUpdateConcurrencyException by saving from two contexts
  • Write the handler: return 409 from the API with the current database values, and show a conflict message in Angular
  • Implement a database-wins and a client-wins resolution, then speak the answer including the pessimistic alternative

Deliverable: A working conflict scenario end to end, with both resolution strategies and a recorded spoken answer.

Day 4 — Abstract class versus interface, and the reduce clone

  • Write a small example in C# where an interface and an abstract base class both make sense, and note which you would pick and why
  • Implement your own reduce in TypeScript, with tests for a missing initial value, an empty array, sparse arrays and the index and array arguments
  • Time yourself explaining your reduce implementation aloud while typing it from scratch

Deliverable: A C# example with a written trade-off paragraph, and a tested reduce clone you can rebuild from memory.

Day 5 — Hands-on Web API and Angular, plus query performance

  • Build one feature under a time limit: a validated create and list endpoint with a DTO, consistent error responses, an Angular form and list, and one test on each side
  • Review your code as if it were going to production and write the refactors you would make for scale

Deliverable: A finished feature with tests, a written refactor list, and notes on how each of the two SQL practice questions applies to a slow API endpoint.

Day 6 — Behavioral stories

  • Write three stories with your decision, the alternative you rejected and the result, drawn from API, Angular or data access work
  • Write a story about choosing between an interface and an abstract class, and one about optimizing a slow endpoint

Deliverable: Three written stories and two spoken practice answers, each ending with a result.

Day 7 — Full dry run and HR logistics

  • Run a mock: rebuild the reduce clone and extend a Web API endpoint with an Angular change, narrating your reasoning throughout
  • Answer the concept questions on lazy versus eager loading, abstract class versus interface and concurrency from memory
  • Write your notice period, location constraints and expectations, and two questions about the team's work for the final discussion

Deliverable: A narrated mock, a pass or fail list of the concept answers, and a one-page HR sheet.

Behavioral preparation for Agilité Solutions

Candidates describe the behavioral stage as an assessment of team compatibility and cultural fit, and the role description they report stresses explaining technical ideas clearly, accountability and keeping stakeholders informed. Prepare stories where you owned a technical decision on APIs, Angular screens or data access, and can state what you chose, what you rejected and what happened.

  1. Describe a scenario where you had to choose between an interface and an abstract class.
  2. Explain how you would optimize a slow-performing API endpoint.
  3. Tell me about a time you took code from a working prototype to something you were willing to deploy, and what you added.
  4. Describe a time you had to clarify unclear requirements with a stakeholder before building, and what changed as a result.
  5. Walk through a design pattern or library you chose in a past project or assignment, and the alternative you rejected.

How do you handle concurrency in EntityFramework?

Medium · Technical Foundations and Frameworks

Approach

  1. Name the problem first: two users load the same row, both edit it, and the second save silently overwrites the first (a lost update). Entity Framework Core uses optimistic concurrency, so it holds no lock between the read and the write; it detects the conflict at save time.
  2. Add a concurrency token. On SQL Server that is a rowversion column, mapped with [Timestamp] on a byte[] property or IsRowVersion() in the model builder; for non-rowversion databases mark chosen properties with [ConcurrencyCheck] or IsConcurrencyToken(). EF then adds the token to the WHERE clause of UPDATE and DELETE statements, and when zero rows match it throws DbUpdateConcurrencyException.
  3. Handle the exception with an explicit policy. Catch it, read entry.OriginalValues, entry.CurrentValues and await entry.GetDatabaseValuesAsync(), then choose database wins (reload and discard the edit), client wins (copy database values into OriginalValues and save again) or a field-by-field merge. In a Web API, return 409 Conflict (or 412 with an ETag and If-Match header) and let Angular show the user what changed.
  4. In a Web API the token must travel with the DTO to Angular and back, and you must assign it to the entity's OriginalValue before saving. If you reload the row fresh and save, the check compares the token against itself and never fires. Cover deletes, and use bounded retries only for merges that are safe to repeat.
  5. Know the pessimistic alternative and its cost: a transaction at Serializable or Snapshot isolation, or raw SQL with UPDLOCK, holds locks and risks blocking and deadlocks. For counters or stock, an atomic ExecuteUpdate such as SET Stock = Stock - 1 WHERE Stock > 0 avoids the conflict entirely. Optimistic suits low contention; pessimistic suits hot rows where retries would pile up.

Follow-up

  • Why must the row version be sent to the Angular client and returned with the update request?
  • When would you pick pessimistic locking or an atomic ExecuteUpdate over a concurrency token?
  • What goes wrong with an unbounded retry loop around DbUpdateConcurrencyException?

Ship under a deadline and bound the debt you chose

Medium · pagination · technical debt · tradeoffs

You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.

Approach

  1. Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
  2. Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
  3. Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
  4. Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.

Follow-up

  • At what page depth does the offset version breach your latency budget, given your page size and row counts?
  • What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?

Tell callers you do not own that their integration breaks

Medium · deprecation · compatibility · stakeholders

A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.

Approach

  1. Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
  2. Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
  3. Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
  4. Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.

Follow-up

  • How would you detect a consumer that reads the field only during a monthly export?
  • One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?