cswork / libraryOpen practice workspace

Interview guides · Aj Bell

Aj Bell Software Engineer Interview Guide

Aj Bell Software Engineer interview guide: four reported rounds, take-home API and codebase walkthrough prep, .NET and JavaScript questions, 7-day plan.

The 60-second brief

Aj Bell runs an investment platform in the UK. Candidate reports say the systems behind it handle financial transactions, user portfolios and sensitive personal data, and that the people using the platform include customers and financial advisers who manage investments through it. In the role described to candidates, Software Engineers write backend logic in .NET (C#), build APIs, and implement frontend features in JavaScript, owning their code from design through to deployment. They work in cross-functional teams with product managers, QA engineers and other developers, take part in code reviews and agile ceremonies, and turn business requirements written in financial vocabulary into working features. Candidates also describe refactoring older code and meeting security and compliance standards as part of the job.

This guide is for engineers with working .NET (C#) and JavaScript experience, ideally including building and consuming REST APIs, who are preparing for the Software Engineer loop at Aj Bell. Candidates describe the level as mid-to-senior. It covers all four reported stages, the foundational, system design, code-defense and behavioral questions candidates list, and a plan for the take-home and the walkthrough that follows it. It does not cover other roles at Aj Bell.

Candidates report four stages over roughly three to five weeks: Initial Screening, First-Round Interview, Technical Assessment (a take-home or a live two-hour coding round) and a Codebase Walkthrough of your own submission.

Focus areas

  • System Design
  • C#
  • API Development

Overview

The work candidates describe is backend-heavy: REST APIs in .NET, some JavaScript on the frontend, and code that touches money, portfolios and personal data. That shapes what to practise. Expect questions on framework fundamentals (dependency injection in .NET Core, asynchronous programming in JavaScript and .NET, LINQ), on securing and scaling an API, and on how you test your own work before QA sees it. The topics reported most often for this loop are system design, C#, API development, REST APIs and consuming web APIs.

What makes this loop distinctive is that your own code becomes the interview. On the take-home route, the final stage has you share your screen and walk a panel through the project, and the reported questions are about your folder and namespace layout, a specific block of logic, how nulls and unexpected input are handled, what you would refactor, and how the API would cope with ten times the traffic. Build the assignment so that every choice has a reason you can say aloud, and keep a short log of decisions and the alternatives you rejected while you work. Some teams use a live two-hour coding round instead, so ask early which one you have, because the preparation differs.

Design and behavioral questions complete the picture. The design questions candidates list include handling a traffic spike when markets open, the database design for a user portfolio feature, monolith versus microservices, and an open problem statement to solve with a framework of your choice. Behavioral questions cover disagreeing with a senior developer's architectural decision, learning a complex domain or its terminology quickly, your ideal work environment and why you want this role. Candidates report that the interview style ranges from collaborative to blunt, so practise giving short, direct answers calmly, and learn basic investment-platform vocabulary so the wording of a problem statement does not slow you down.

The Aj Bell interview process

Candidates describe four stages in this order. The technical assessment is either a take-home, commonly an API built from a problem statement, or a live two-hour coding round, and the final walkthrough is reported for candidates who did the take-home.

Initial Screening

Candidates report that the process opens with a screening that can include a brief numeracy and literacy test, with a first-round interview following. Nothing here needs deep technical revision, but a vague answer about your background or why you want the role costs you early. Use the contact to find out which format the technical assessment will take, since candidates report both a take-home and a live coding round.

What to demonstrate

  • Basic numeracy and literacy, which is what a short test of that kind inherently measures
  • How concisely you summarise your background for a .NET and JavaScript role
  • A clear account of why you want this role

How to prepare

  • Refresh percentages, ratios and reading figures from small tables so a numeracy test does not cost you through arithmetic slips.
  • Write a spoken summary of your career that names the .NET and JavaScript work you owned, and rehearse it until it is short and fluent.
  • Write two or three specific, checkable reasons for applying so the answer to why this role does not sound generic.
  • Ask the recruiter whether the technical assessment is a take-home or a live coding round, and what the final stage involves.

First-Round Interview

Candidates report a conversation with two senior developers about your past experience, the details of your resume, and foundational technical questions in .NET and JavaScript. Because the discussion is anchored in your CV, every project and technology you list is open for detail: be ready to say what you built, what you personally owned and which decision you would defend. The foundational questions candidates list include dependency injection in .NET Core, asynchronous operations in JavaScript, designing a secure and scalable API, and testing your own code before QA.

What to demonstrate

  • Hands-on depth behind the .NET and JavaScript experience on your resume
  • Command of framework fundamentals such as dependency injection, asynchronous code and LINQ
  • How clearly you explain past decisions and separate your own contribution from the team's

How to prepare

  • For each project on your CV, write what you built, what you owned and one trade-off you made, and cut anything you cannot discuss in depth.
  • Practise explaining dependency injection (registration, constructor injection, transient, scoped and singleton lifetimes) and synchronous versus asynchronous code in .NET aloud.
  • Rehearse the JavaScript async answer: event loop, promises, async/await, error handling, and parallel versus sequential calls.
  • Prepare a short answer on how you test before QA: unit tests, integration tests against the API, and what you still check by hand.

Technical Assessment

Candidates report that most are given a take-home assignment, such as building an API from a specific problem statement, while some teams run a live two-hour coding round instead. Candidates describe the take-home as average in difficulty for anyone who has built APIs, and as taking a few hours. Because a take-home is followed by a walkthrough of the same code, build it as a project you will have to defend: clear structure, input validation, error handling, tests and a short README of assumptions.

What to demonstrate

  • Working, runnable code that solves the problem statement you were given
  • API design: request and response handling, security or authentication, and error handling
  • Code structure, naming and tests, which a submitted project shows directly

How to prepare

  • Before coding, list the assumptions and unfamiliar domain terms in the problem statement and record them in a README so a financial term does not stall you.
  • Build a small REST API in .NET from a problem statement of your own, with validation, consistent error responses and tests, and treat it as production code.
  • Keep a running decision log of each structural choice, the alternative you rejected and the reason, because the walkthrough asks for exactly that.
  • For the live format, rehearse a compressed sequence: clarify requirements, outline endpoints and models, get one path working end to end, then add validation and tests.

Codebase Walkthrough

For candidates who completed the take-home, the final stage is reported as a walkthrough with a panel. You share your screen, navigate your own codebase and explain your reasoning, thought process and architectural decisions. Reported questions cover why the folders and namespaces are organised as they are, the logic in a specific block, what you would refactor with more time, how null and unexpected values are handled, how the error handling works, and what would change if the API had to take ten times the traffic.

What to demonstrate

  • Understanding of your own implementation, including why it is structured the way it is
  • Whether you can name the trade-offs behind a decision and tie it to maintainability, scalability or readability
  • How the solution behaves with null, malformed or unexpected input
  • Your testing approach for the API you built

How to prepare

  • Rehearse the walkthrough aloud on a screen share, opening files in the order you will explain them, until you can navigate without hunting.
  • For each folder, namespace, controller and data-access choice, write one line of reasoning and one alternative you rejected.
  • Trace the null, empty and malformed-input paths in your own code, note the test covering each, and close the gaps before you submit.
  • Write your ten-times-traffic answer and your refactor list in advance: where the first bottleneck sits, what you would cache or move off the request path, and what you would clean up first.

Where candidates lose points

Submitting a take-home with structure, patterns or libraries you cannot explain line by line

Before the walkthrough, go through every folder, namespace, class and branch and write one line of reasoning plus the alternative you rejected. Remove or rewrite anything you cannot defend. Reported walkthrough questions ask why the folders and namespaces are laid out as they are and for the reasoning behind a specific block of logic.

An API that works on the happy path but breaks on null, empty or malformed input

Add validation at the boundary, return consistent error responses, and write tests for missing fields, nulls and unexpected values. Document the behaviour in the README so you can answer how the solution handles edge cases without opening the code to look.

Answering a design prompt by naming components before clarifying the problem

For the market-opening traffic spike, the portfolio database design and monolith versus microservices, first ask who calls the system, which paths read and which write, and what must stay consistent. Then choose components and state the trade-off each choice makes, including why a monolith can be the right answer.

Giving a thin answer to a fundamentals question such as JavaScript async handling or .NET dependency injection

Prepare each fundamentals answer with a mechanism, an example and a failure case: the event loop and an output-order example, the service lifetimes and what goes wrong when a scoped service is held by a singleton. Say it aloud, because written notes hide gaps.

Generic behavioral answers that name no decision, no evidence and no outcome

Use a story with a real technical decision, your evidence and an honest result for the disagreement question, and a method plus shipped outcome for the learning-a-domain question. Prepare the why-this-role answer from facts about the role, and practise answering short, blunt questions without filler.

Questions & practice

Explain your reasoning and thought process for this specific block of logic.

Medium · Code Defense and Walkthrough

Approach

  1. Restate the block's contract in one sentence before going line by line: its inputs, its output, the invariant it maintains and any side effects (database writes, external calls).
  2. Explain the alternatives you weighed and why this one won, with the cost: for example a Dictionary lookup at O(1) average per item instead of a nested scan at O(n·m), or a plain loop instead of LINQ where clarity or allocations mattered.
  3. Know the runtime behaviour of what you wrote: LINQ queries are deferred, so enumerating an IEnumerable twice runs it twice (and with EF Core can hit the database twice); async code should be awaited end to end rather than blocked with .Result or .Wait(), and a CancellationToken should flow through.
  4. Name the edge cases the block handles and point to the test that covers each; if one is not covered, say so before being asked.
  5. If you would write it differently now, say what and why; owning a weakness reads better than defending it.

Follow-up

  • What happens if two requests run this block at the same time?
  • What is the time and memory cost on a very large input?
  • How would you unit-test this block in isolation?

Merge partitioned event streams into one ordered feed with bounded lateness

Hard · k-way merge · watermarks · out-of-order streams

The read-model service consumes 64 log partitions carrying about 4,000 events per second in total. Each partition is ordered within itself, but partitions drift by up to 30 seconds, and the activity feed must present a tenant's events in occurred_at order. Produce the merge. State its complexity, the buffer it requires in events and in bytes, what happens when one partition is idle, and what you do with an event that arrives after you have already emitted its position. Payloads average 1 KB.

Approach

  1. Merge with a min-heap over the 64 partition heads keyed on (occurred_at, event_id): O(log P) per event and O(n log P) overall. The tie-break on event_id is what makes the output deterministic when two partitions carry the same millisecond, which matters because the feed is paginated and a non-deterministic order reorders pages under the reader.
  2. Emitting the heap head is only correct once every partition has produced everything up to that timestamp, so the emit condition is a watermark: the minimum across partitions of the highest occurred_at seen, less the allowed lateness. Events are held until the watermark passes them, which is what turns individually ordered streams into a jointly ordered one.
  3. Size the buffer from the lateness rather than guessing: 4,000 events per second times 30 seconds is 120,000 buffered events, and at 1 KB each about 120 MB of heap. That number is the real price of the ordering guarantee and belongs in front of whoever asked for it.
  4. Handle the idle partition explicitly, because it fails the feed rather than corrupting it: a partition with no traffic never advances its own maximum, so the watermark freezes and output stops entirely. Either every partition emits a periodic idle marker carrying the broker's current time, or the watermark falls back to wall clock for a partition silent beyond a threshold.

Follow-up

  • The lateness budget is raised to five minutes. What is the new buffer, and what besides memory changes?
  • The consumer restarts. Where does it resume from, and what does the feed look like for the first 30 seconds?

Canonicalise a request body into a stable idempotency fingerprint

Medium · parsing · canonicalisation · hashing

idempotency_key.request_fingerprint is a SHA-256 over the method, path and canonicalised body, and a retry whose fingerprint differs must be rejected with 422 rather than served the stored response. Write the canonicaliser. Bodies are JSON up to 256 KB nested at most 32 levels; clients vary key order, whitespace and unicode escaping, and some send 64-bit ids as JSON numbers. Produce a deterministic byte string such that semantically identical bodies match and any semantic difference does not. State your complexity and name two normalisations you refuse to perform.

Approach

  1. Parse once into a tree, then re-serialise under fixed rules: object keys sorted, array order preserved, one escaping convention, no insignificant whitespace. Parsing is O(n) and sorting keys is O(k log k) per object, so O(n log n) overall with O(depth) stack, and the 32-level cap is enforced during parsing because hostile nesting is how a canonicaliser becomes a stack overflow.
  2. Sort keys by their UTF-8 bytes and say why the obvious implementation is wrong in some runtimes: a default string comparison that orders by UTF-16 code units places surrogate pairs, meaning code points from U+10000 up, below U+E000 to U+FFFF, which is not UTF-8 byte order, so two services written in different languages disagree on the same document.
  3. Do not re-encode numbers through a double. IEEE-754 binary64 represents integers exactly only up to 2^53, so normalising a 19-digit id through a float changes it, and 1 against 1.0 cannot be reconciled without deciding whether they are the same value. Preserve the literal token, and require ids as strings at the API boundary if you want them comparable.
  4. Reject duplicate keys rather than picking one. JSON permits them and parsers disagree, most keeping the last, so any choice you make ties the fingerprint to a parser detail that the code handling the request does not necessarily share.

Follow-up

  • A client sends the same logical request with an extra field your API ignores. Same key, different fingerprint, so you return 422. Is that the right answer?
  • Where does the fingerprint get computed relative to request decompression and the body-size limit?

Stop tag and share joins from fanning out a page

Medium · join fan-out · semi-joins · many-to-many

resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.

Approach

  1. Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
  2. Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
  3. Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
  4. Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.

Follow-up

  • The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
  • A resource can be shared with the same user twice under different permissions. Does your count change, and should it?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?

Explain how dependency injection works in a.NET Core application.

Medium · Technical and Framework Fundamentals

Approach

  1. Start with the mechanism: services are registered on IServiceCollection in Program.cs (builder.Services.AddScoped<IPortfolioService, PortfolioService>()), and the built-in IServiceProvider builds the dependency graph and supplies constructor parameters when it activates controllers, middleware and other services.
  2. Explain the three lifetimes precisely: Transient creates a new instance on every resolve, Scoped creates one per scope (one per HTTP request in ASP.NET Core), Singleton creates one for the application's lifetime. AddDbContext registers the DbContext as Scoped by default.
  3. Name the captive-dependency bug: a Singleton that takes a Scoped service (such as a DbContext) keeps it alive across requests. Scope validation, on by default in the Development environment, throws when it detects this; the fix is to inject IServiceScopeFactory and create a scope.
  4. Say why it matters: classes depend on interfaces, so tests can pass a mock repository, and configuration arrives through IOptions<T> instead of static lookups. The container also disposes the IDisposable instances it created when their scope ends.
  5. Mention resolution details: registering the same interface twice means a single resolve returns the last registration, while injecting IEnumerable<T> returns all of them; .NET 8 adds keyed services ([FromKeyedServices]) for choosing a named implementation.

Follow-up

  • How would you use a Scoped DbContext inside a BackgroundService, which is registered as a Singleton?
  • What is the difference between constructor injection and calling GetService (the service locator pattern), and why is the latter discouraged?
  • When would you reach for a third-party container instead of the built-in one?

What are the key considerations when designing a secure and scalable API?

Medium · Technical and Framework Fundamentals

Approach

  1. Split authentication from authorisation: OAuth 2.0 / OpenID Connect with short-lived JWT bearer tokens, validating signature, issuer, audience and expiry; then check on every request that the caller owns the account or portfolio in the URL, since broken object-level authorisation is first on the OWASP API Security Top 10.
  2. Cover the input and transport layer: TLS everywhere, validation of every field at the boundary, no account numbers or tokens in query strings or logs, and rate limiting per client (ASP.NET Core has built-in rate-limiting middleware since .NET 7).
  3. For scale, keep the API stateless so instances can sit behind a load balancer and scale horizontally; use async I/O, paginate list endpoints, and cache read-mostly responses with Cache-Control and ETags.
  4. For operations that move money, require an idempotency key so a retried POST returns the original result instead of executing twice; return consistent problem-details error bodies (RFC 9457, which replaced RFC 7807).
  5. Close with evolution and operations: a versioning scheme (URL segment or header), correlation IDs propagated through logs, and an audit trail of who changed what.

Follow-up

  • How would you make an order-placement endpoint safe for a client to retry after a timeout?
  • A JWT is valid for an hour and the user's access is revoked now: how do you handle that?
  • Would you rate-limit by IP, by client ID or by user, and what does each miss?

How do you approach testing your own code before it goes to QA?

Medium · Technical and Framework Fundamentals

Approach

  1. Describe the layers you write yourself: unit tests (xUnit or NUnit for C#, Jest for JavaScript) on domain logic with dependencies mocked through interfaces (Moq or NSubstitute), using [Theory] with [InlineData] to cover boundaries in one table.
  2. Add integration tests through WebApplicationFactory<TProgram> (Microsoft.AspNetCore.Mvc.Testing), which hosts the API in memory so real routing, model binding, filters and serialisation run; back them with a real database (for example Testcontainers) because the EF Core InMemory provider does not enforce relational constraints or transactions.
  3. List the edge cases you deliberately test: empty and null inputs, zero and negative amounts, decimal rounding on money (decimal, never double), duplicate submissions, dates near midnight and across time zones, and not-found or unauthorised access to someone else's record.
  4. Before handing over: check each acceptance criterion against the running app (Swagger or Postman), read your own diff as a reviewer would, and let CI run the tests and static analysis so the build is green before QA sees it.
  5. Treat coverage as a gap-finder, not a target: high line coverage with no assertions on behaviour proves little.

Follow-up

  • What do you mock and what do you deliberately leave real?
  • How do you test code that depends on the current time? (Injecting TimeProvider in .NET 8, or an IClock interface.)
  • An integration test passes locally and fails intermittently in CI: how do you find the cause?

Here is a problem statement. Using the framework of your choice, design a real-life system to solve it.

Medium · System Design and Architecture

Approach

  1. Spend the first minutes on requirements: who the users are, the core use cases, expected read and write volume, what must be strongly consistent (balances, orders) and any audit or regulatory constraint. Write functional and non-functional requirements down before drawing anything.
  2. Define the API surface and the data model next, because they anchor every later choice; then name your framework and justify it, for example ASP.NET Core Web API with EF Core over a relational database for transactional data.
  3. Draw the components and walk one request end to end: client, load balancer or gateway, stateless API instances, relational database, a cache for hot reads, and a message queue for work that does not need to finish inside the request.
  4. Then go looking for failure: the database as a single point of failure (replicas, failover), retries made safe with idempotency keys, and the transactional outbox pattern so a database write and its published event cannot diverge.
  5. Finish with trade-offs and what you would leave out of a first version, so the design stays proportionate to the problem statement.

Follow-up

  • How would you make the system highly available and fault tolerant?
  • Which component would you split out first if load grew tenfold, and why that one?
  • How would you change the database schema without downtime?

How would you architect a system that needs to handle sudden spikes in user traffic during market opening hour

Medium · System Design and Architecture

How would you architect a system that needs to handle sudden spikes in user traffic during market opening hours?

Approach

  1. Characterise the spike first: it is predictable (it arrives at the same time each trading day), and it mixes heavy reads (prices, portfolio valuations) with bursty writes (orders). Predictability means you can pre-scale on a schedule before the open instead of relying only on reactive autoscaling, which lags behind a sudden ramp.
  2. Absorb reads: cache quotes and valuations in a distributed cache such as Redis with short TTLs, push price updates over WebSockets (SignalR in .NET) instead of letting every client poll, and send reporting queries to read replicas.
  3. Level writes: validate the order, persist it, put it on a durable queue and return 202 Accepted with a status URL; workers drain the queue at the rate downstream systems can handle. Idempotency keys stop client retries from creating duplicate orders.
  4. Protect the core: rate limiting per user, circuit breakers and timeouts on downstream calls (Polly in .NET), sized database connection pools, and load shedding of non-essential features (for example analytics widgets) when latency climbs.
  5. Prove it with a load test that replays the shape of the open (a steep ramp, not a flat average) and alert on p99 latency, queue depth and error rate.

Follow-up

  • Many cached prices expire at the same moment at the open: how do you avoid a cache stampede on the database? (Request coalescing, jittered TTLs, refresh-ahead.)
  • The downstream execution venue slows down: what does the user see and how does the queue behave?
  • How do you stop a client that retries on timeout from placing the same order twice?

Walk me through the database design for a user portfolio management feature.

Medium · System Design and Architecture

Approach

  1. Name the core entities: Customer, Account (with an account type and base currency), Instrument (identifier such as ISIN, currency), Transaction (buy, sell, deposit, withdrawal, dividend, fee) and Price (instrument_id, price_date, price).
  2. Make the transaction table an append-only ledger and derive holdings from it; keep a Position table (unique on account_id plus instrument_id) updated in the same database transaction for fast reads, and correct mistakes with reversing entries rather than updates or deletes, so history stays auditable.
  3. Choose types carefully: decimal columns with enough precision for money and fractional unit quantities (never float), a currency on every amount, and UTC timestamps with a separate trade date and settlement date.
  4. Index for the real queries: (account_id, trade_date) on transactions for statements, (instrument_id, price_date) as the price key for valuation joins; valuation is positions joined to the latest price per instrument.
  5. Handle concurrent updates to the same position with optimistic concurrency (a rowversion column used as an EF Core concurrency token) or a row lock, and say which you chose and why.

Follow-up

  • How would you compute a portfolio's value as of a date six months ago?
  • How does the schema handle a stock split or another corporate action?
  • A customer has many thousands of transactions and the portfolio page is slow: what do you change?

Explain the trade-offs between using a monolithic architecture versus microservices for our platform.

Medium · System Design and Architecture

Approach

  1. Give the monolith its due: one deployable, in-process calls, ACID transactions across modules, simple local debugging and a single pipeline. Its costs are coupled releases, scaling the whole application to relieve one hot path, and a codebase that gets harder to change as teams grow.
  2. Give microservices the same treatment: independent deployment and scaling, fault isolation and team autonomy, paid for with network failures and latency, no cross-service ACID transactions (sagas and eventual consistency instead), distributed tracing, and the operational load of containers, orchestration and a CI/CD pipeline per service.
  3. State the deciding factors: team size and number of teams, how clear the domain boundaries are, whether one part has a distinct scaling or release profile, and how much consistency the domain needs. Where money moves, cross-service consistency is the expensive part.
  4. Offer the middle path: a modular monolith with enforced module boundaries (separate projects, no shared tables), extracting a service with the strangler fig pattern only when a module has a measured reason to stand alone.
  5. Ground it with an example: a price-feed or notification component that scales differently from account management is a natural first extraction; the ledger is a poor one.

Follow-up

  • An order touches the account service and the payments service: how do you keep them consistent without a distributed transaction?
  • What signals would tell you a module is ready to be extracted?
  • During a migration, two services still share one database: what risks does that carry and how do you retire it?

Walk me through the codebase you submitted. Why did you structure the folders and namespaces this way?

Medium · Code Defense and Walkthrough

Approach

  1. Open with a short map before diving in: the entry point (Program.cs and service registration), then each project or top-level folder and the direction of dependencies, for example Api referencing Application, Application referencing Domain, Infrastructure implementing interfaces defined inside.
  2. Name the organising principle and why you chose it: layered or clean architecture (separation by technical concern, testable core) versus vertical slices by feature (everything for one use case together). Say honestly if a full layered split is heavier than a small exercise needs and what it bought you.
  3. Explain that namespaces mirror folders (the C# convention and IDE default), and that separate projects let the compiler enforce boundaries that folders alone cannot.
  4. Trace one request through the tree: controller, validation, application service or handler, repository, database, and where mapping and error handling sit. This shows the structure works, not just that it looks tidy.
  5. Point to the test projects and show they mirror the production structure, so the reader can find the tests for any class.

Follow-up

  • Where would you add a new feature, for example a withdrawal endpoint, and which files would it touch?
  • Why separate projects rather than folders in one project?
  • What would you change about this structure if ten developers worked in it?

If you had more time, what would you improve or refactor in this take-home test?

Medium · Code Defense and Walkthrough

Approach

  1. Arrive with a prioritised list rather than improvising: correctness gaps first (missing validation, unhandled concurrency, weak error handling), then test coverage of the paths that matter, then operability, then structural refactors.
  2. For each item state what, why and cost, for example replacing an in-memory store with a real database plus migrations, adding authentication, pagination on list endpoints, idempotency on writes, structured logging, health checks and secrets moved out of appsettings.
  3. Separate shortcuts you took knowingly for time from things you now think were wrong; both are fine to admit, but they show different things about your judgment.
  4. If scale comes up, keep the answer concrete: stateless instances, database indexes for the actual queries, caching of read-mostly data, and async processing for slow work.
  5. Describe how you would make the refactor safe: tests around current behaviour first, small steps, each one shippable on its own.

Follow-up

  • If you only had one more hour, which single change would you make first and why?
  • How would you carry out that refactor without breaking existing behaviour?
  • Is there anything in the submission you would remove rather than add?

How does your solution handle edge cases or unexpected null values?

Medium · Code Defense and Walkthrough

Approach

  1. Describe the compile-time defence first: nullable reference types enabled (<Nullable>enable</Nullable>) so the compiler warns on possible null dereferences, and guard clauses such as ArgumentNullException.ThrowIfNull (.NET 6+) at public boundaries.
  2. Validate at the API boundary: with [ApiController], an invalid model state returns a 400 with ValidationProblemDetails automatically; use data annotations or FluentValidation, and use nullable value types with [Required] so a missing amount is not silently bound as 0.
  3. List the edge cases you handled and the response each gets: empty collections, zero or negative amounts, duplicates, very large values or overflow, unknown IDs (404), concurrent modification (409) and date or time-zone boundaries.
  4. Show the safety net: a global exception handler (UseExceptionHandler, or IExceptionHandler in .NET 8) that logs with a correlation ID and returns a problem-details body without leaking stack traces.
  5. On the JavaScript side, mention optional chaining (?.) and nullish coalescing (??), noting that ?? falls back only on null or undefined, while || also replaces 0 and empty strings, which corrupts numeric fields.

Follow-up

  • Should a method return null or an empty list when there are no results, and why?
  • Where should validation live: the controller, the application layer or the domain model?
  • An external API you call starts returning null for a field documented as required: what does your code do?

Listing latency scales with page size, not with filters

Easy · n+1 · latency · query batching

The tenant listing endpoint reads resource filtered by tenant_id and status, ordered by updated_at DESC, and returns each row plus the owner's display name from app_user and the actor of that resource's latest resource_revision. p99 is 55 ms at 10 rows per page and 1.4 s at 200. Database telemetry shows 401 statements per request, each under 1 ms, and nothing in the slow-query log. Diagnose the cause and give the fix, stating the statement count per request and the p99 you expect afterwards.

Approach

  1. Read the counters before forming a theory. 401 statements for 200 rows is one driver query plus two per row, and sub-millisecond execution with an empty slow-query log rules out a bad plan. The time is round trips, which is why it is invisible in every per-query metric and scales with rows returned rather than with filter selectivity.
  2. Name the two per-row statements from their normalised text: a single-row app_user lookup by user_id, and a resource_revision lookup by resource_id ordered by version DESC LIMIT 1. Confirm by dropping those two response fields and watching the statement count fall to one. That locates the calls in the serialisation layer, not the repository.
  3. Check that the arithmetic accounts for the whole gap. Measure one round trip to the replica in isolation; 400 trips at roughly 3 ms of network plus 0.2 ms of execution is about 1.3 s on top of a 55 ms baseline, which matches. If the multiplication had fallen short, the N+1 would only be part of the story and you would keep looking.
  4. Batch both lookups. Collect owner_user_ids and resource_ids from the driver query, then issue WHERE tenant_id = $1 AND user_id = ANY($2) for the users, and PostgreSQL's SELECT DISTINCT ON (resource_id) ... WHERE resource_id = ANY($2) ORDER BY resource_id, version DESC for the latest revision, which the UNIQUE (resource_id, version) index serves directly. On an engine without DISTINCT ON, use a lateral join or a row_number window. Three statements per request at any page size.

Follow-up

  • The page size is capped at 200 today. What breaks first if it is raised to 2,000, and is it still this bug?
  • How do you stop the next N+1 from reaching production, given that no individual query is slow and the endpoint's tests pass?

Suggested practice checklist

The plan follows the loop backwards from the walkthrough: first fundamentals and your CV, then building and hardening an API you can defend, then system design, then rehearsing the screen-share walkthrough and the behavioral answers.

Day 1 — Map the loop and prepare your CV story

  • Write the four-stage map (Initial Screening, First-Round Interview, Technical Assessment, Codebase Walkthrough) and note the questions to ask the recruiter about the assessment format.
  • Write your CV walkthrough for the .NET and JavaScript projects you will discuss, naming what you owned and one trade-off per project, then say it aloud.
  • List 15 investment-platform terms (for example portfolio, holding, order, trade, settlement) with a one-line definition each.

Deliverable: A one-page loop map with recruiter questions, a written and rehearsed CV walkthrough, and a 15-term glossary.

Day 2 — Fundamentals: JavaScript async and .NET dependency injection

  • Write five small JavaScript snippets mixing console.log, setTimeout, promise chains and await, predict the output order of each, then run them to check.
  • Write one runnable example each of Promise.all, Promise.allSettled and AbortController cancellation, with a failing call in each.
  • Explain aloud how dependency injection works in .NET Core: registration, constructor injection, transient, scoped and singleton lifetimes, and how it helps testing.
  • Write a half-page comparison of synchronous and asynchronous code in .NET, including what await does to the calling thread.

Deliverable: Five predicted-then-verified snippets, three runnable concurrency and cancellation examples, a spoken dependency injection answer and a written sync versus async comparison.

Day 3 — Build the take-home style API

  • Invent a problem statement with a financial flavour, such as an API for portfolio holdings and transactions, and write its assumptions in a README before coding.
  • Build the endpoints in .NET with a layered folder structure, input validation and consistent error responses.
  • Write unit tests and at least one integration test for the main flow, and note how you would test before handing code to QA.

Deliverable: A working .NET API with tests and a README of assumptions, plus a finished solution to the idempotency-fingerprint practice question.

Day 4 — Harden the API: edge cases, security and scale

  • Add tests and fixes for null, empty, oversized and malformed input, and record what each case returns.
  • Write a security checklist for a public API handling sensitive financial data: authentication, authorisation, input validation, rate limiting, secrets handling and logging that avoids sensitive values.
  • Write your ten-times-traffic answer for this API: first bottleneck, caching, stateless instances, work moved off the request path, and data-access changes.

Deliverable: Edge-case tests with documented behaviour, a security checklist, a written ten-times-traffic answer and a worked debugging practice question.

Day 5 — System design: portfolio schema, traffic spikes, architecture trade-offs

  • Design for a surge of traffic when markets open: separate read and write paths, cache read-heavy data, queue writes, balance load, and decide what degrades first.
  • Write a monolith versus microservices comparison with the deciding factors: team size, independent deployment, data consistency and operating cost.
  • Take an open problem statement, pick a framework, and outline components, data model and failure handling in one pass.

Deliverable: A portfolio schema with indexes, a traffic-spike design, a monolith versus microservices comparison and one open-problem design outline.

Day 6 — Rehearse the codebase walkthrough

  • Share your screen and walk through your Day 3 and 4 project aloud, explaining why the folders and namespaces are organised as they are.
  • Pick three blocks of logic and explain the reasoning behind each, including the alternative you rejected.
  • Write the list of what you would refactor with more time, in priority order, with the reason for each.
  • Record the session, listen back, and note every place you hesitated or said it just works.

Deliverable: A recorded walkthrough, a decision log per file or folder, and a ranked refactor list.

Day 7 — Behavioral answers and a full mock

  • Write structured notes for the disagreement with a senior developer and the learn-a-new-domain stories, each with evidence and an honest outcome.
  • Write and rehearse your answers to the ideal work environment and why you want this role questions, using facts about the role.
  • Run a mock with a friend who asks short, blunt questions about your project and your CV, and practise answering directly and concisely.
  • Review your Day 1 map and mark any stage you still feel unready for.

Deliverable: Written notes for four behavioral answers, a recorded mock session and a final list of weak spots to fix before the interviews.

Behavioral questions candidates report

The behavioral questions candidates report are about how you work with senior colleagues, how fast you pick up an unfamiliar domain, what kind of environment suits you and why you want this role. The role is described as cross-functional, with product managers, QA engineers and other developers, and candidates report an interview style that ranges from collaborative to blunt. Prepare stories with a real technical decision and an honest outcome, and keep answers direct.

  1. Tell me about a time you had to explain a complex technical issue to a non-technical stakeholder.
  2. Walk me through a project where you faced a significant technical roadblock. How did you overcome it?

Walk me through your resume and highlight your experience with.NET and JavaScript.

Medium · Technical and Framework Fundamentals

Approach

  1. Open with a short spoken timeline of roles in order, then spend most of the answer on two or three projects where .NET (C#) and JavaScript were both in play. For each, say what the system did, what you personally built or decided, and what the outcome was.
  2. Be exact about the stack: which .NET flavour you used (Framework or .NET Core), where JavaScript sat (browser UI, build tooling, Node), and how the two talked, usually over REST APIs. A vague list of technologies reads as thin experience.
  3. Attach one decision and its trade-off to each project, such as how you structured data access in a controller or how the frontend handled async calls. Use I for your own work and name the team separately.
  4. Treat every line of the resume as open to detail, and cut or prepare anything you could not discuss in depth. Close by linking your experience to the work this role is described as doing: backend APIs and frontend features on a financial platform.

Follow-up

  • Which of those decisions would you make differently now, and why?
  • Tell me about a bug in your .NET code that was hard to find. How did you track it down?
  • How did your JavaScript frontend authenticate to the API and handle failed requests?

How do you handle asynchronous operations in JavaScript?

Medium · Technical and Framework Fundamentals

Approach

  1. Start from the model: JavaScript runs on one thread with a call stack, and asynchronous work such as timers, network calls and I/O is handed off and returns through queues. Promise callbacks and await continuations go on the microtask queue, which drains before the next timer callback, so a resolved promise's then runs before setTimeout with a delay of 0.
  2. Show the progression: callbacks (nesting and the error-first convention), promises (then, catch, finally, and chaining returns a new promise), then async/await as syntax over promises with try/catch for errors. Always return or await a promise, otherwise rejections go unhandled and ordering surprises you.
  3. Cover concurrency: await inside a for loop runs calls one after another. Start independent calls together and use Promise.all when all must succeed (it rejects on the first failure but does not cancel the others), Promise.allSettled when partial results are acceptable, and Promise.race for timeouts. Mention limiting concurrency when there are many calls.
  4. Cover failure and cancellation: fetch only rejects on network errors, so check response.ok for 4xx and 5xx. Use AbortController to cancel a request, and guard against out-of-order responses so a slow older response cannot overwrite newer state. If asked to compare with .NET, relate it to Task and await.

Follow-up

  • What order does this print: a console.log, a setTimeout of 0, a resolved promise's then, and a final console.log?
  • Five calls run through Promise.all and one rejects. What happens to the other four, and how would you keep the successful results?
  • How would you stop a search box from displaying the results of an earlier, slower request?

What do you consider to be an ideal work environment for you?

Medium · Behavioral and Competency

Approach

  1. Answer with two or three concrete working conditions, each backed by a short real example, rather than a list of adjectives. For instance: code reviews that give specific, timely feedback, clear requirements from product, and room to ask clarifying questions before building.
  2. Fit it to the role as candidates describe it: cross-functional work with product managers, QA engineers and other developers, plus code reviews and agile ceremonies. Say what you contribute to that environment, such as the review comments you write, the clarifying questions you ask and the documentation you leave behind.
  3. Candidates report that interviewing styles range from collaborative to very direct. Show that direct technical feedback works for you with a short story in which blunt feedback improved your work and you acted on it.
  4. Avoid describing a setting that removes all challenge, and avoid criticising a past employer. Frame your answer as what helps you do your best work, not as a list of things you cannot tolerate.

Follow-up

  • Tell me about a time the environment was the opposite of that. What did you do?
  • How do you react when a reviewer criticises a design you are proud of?
  • What do you need from a manager or senior developer to do your best work?

Tell me about a time you disagreed with a senior developer's architectural decision. How did you handle it?

Medium · Behavioral and Competency

Approach

  1. Choose a decision with real technical stakes, such as a service boundary, a shared database, a chain of synchronous calls or a framework choice. Say what the senior developer chose and why it was a reasonable choice; stating their reasoning fairly shows judgement.
  2. State your concern in technical terms and back it with evidence: the failure mode, the coupling or the cost, ideally supported by a small spike, a measurement or a written comparison of options. Explain how you raised it: privately first, and with questions before conclusions.
  3. Give the outcome honestly. Either the design changed, or you revised your view after learning a constraint you had missed, or you committed to the decision and helped it succeed. Say what happened afterwards, including whether your concern turned out to matter.
  4. What sinks this story is casting the senior as simply wrong, going over their head, or ending with no resolution. Finish with what you learned about disagreeing well, because the question asks how you handled it, not who won.

Follow-up

  • What constraint did they know about that you had not considered?
  • If they had still said no, what would you have done?
  • How did it affect your working relationship afterwards?

Describe a situation where you had to learn a complex new domain or industry terminology quickly.

Medium · Behavioral and Competency

Approach

  1. Pick a real case where unfamiliar vocabulary was blocking your work, ideally a regulated or numbers-heavy domain, and state the stakes: what a misunderstanding would have cost in wrong calculations, rework or bad data.
  2. Describe your method in order: build a glossary of terms, ask a domain expert or analyst to explain each with an example, read existing code, tests and tickets to see how the terms are really used, and sit with someone who works in the domain day to day.
  3. Show that you turned the learning into code: domain terms used as class and method names, assumptions confirmed with the business owner, and a test case built from a real example the expert supplied.
  4. End with evidence, such as what you shipped or an error you caught because of what you learned. For this loop, name the vocabulary you are learning now (portfolio, holding, order, trade, settlement) without claiming expertise you do not have.

Follow-up

  • Which term did you first misunderstand, and what was the consequence?
  • How do you check that a requirement written in domain jargon means the same thing to you and to its author?
  • How would you get up to speed on an investment platform's vocabulary once you had started?

Why do you want to work at Aj Bell, and what do you hope to achieve in this role?

Medium · Behavioral and Competency

Approach

  1. Split the answer into two short parts: why this role, then what you want to achieve in it. Do not speak for the company; talk about what you looked at and why it fits you.
  2. Ground the why in checkable facts about the role as candidates describe it: backend APIs in .NET, JavaScript on the frontend, systems handling financial transactions and portfolios, and ownership from build to deployment. Link each to something you have done or want to learn, and mention only what you actually found in your own research on public sources.
  3. Make the goals specific and sized to the role, for example owning an API area end to end, learning the investment domain well enough to translate requirements without hand-holding, and raising testing or code quality in the code you touch.
  4. Avoid flattery, salary and generic phrases like great culture, and avoid goals that point away from the job. If asked what you hope to achieve, tie it to outcomes the team could see.

Follow-up

  • What else are you considering, and how does this role compare?
  • Which part of this role do you expect to be hardest for you?
  • What would you want to have delivered once you have settled in?