cswork / libraryOpen practice workspace

Interview guides · ArmorCode

ArmorCode Software Engineer Interview Guide

ArmorCode Software Engineer interview guide: the two reported rounds, Java and Spring data access, DNS, DSA questions and a 7-day plan to prepare.

The 60-second brief

ArmorCode builds a platform for Application Security Orchestration and Correlation (ASOC). It gives organizations one place to track their security posture, vulnerabilities and compliance, pulling findings from different security tools together so security and development teams work from the same data. Candidates describe the Software Engineer role as backend feature work on that platform: ingesting, processing and correlating security data, integrating with third-party security tools through their APIs and transforming what they return, reviewing code, taking part in design discussions, and carrying features from design through production deployment and monitoring.

This guide is for candidates interviewing for the Software Engineer role at ArmorCode. It covers the two rounds candidates report, the ten questions reported for them (browser and DNS mechanics, Out of Memory errors, performance troubleshooting, linked lists, sorting, searching, Spring JDBC versus Spring JPA, and database transactions under concurrency), the Java, Spring and SQL stack the role lists, and a seven-day plan. It does not teach security-domain knowledge, because candidates describe that as a bonus rather than a requirement.

Candidates report two rounds over roughly 2-4 weeks: a Technical Assessment, then Deep-Dive Interviews. Reported questions cluster in web and runtime fundamentals, data structures and algorithms, and Java and Spring data access.

Focus areas

  • Data Structures and Algorithms (DSA)
  • DNS fundamentals
  • Linked Lists (reverse operation)

Overview

The work behind this loop is backend engineering on a platform that takes in security findings from many sources, processes them and correlates them. Candidates describe the role as Java with the Spring ecosystem (Spring Boot and Spring Data), SQL optimization, data structures and algorithms, and a working grasp of web and networking fundamentals. Docker, Kubernetes, CI/CD pipelines and automated testing are listed as nice-to-have, as is prior cybersecurity or SaaS experience. For preparation, that means the loop is mostly about fundamentals you can explain from first principles, not about security trivia.

The ten reported questions fall into three groups. The first group is web and runtime mechanics: what happens when a URL is typed into a browser, how a DNS lookup works, what causes an Out of Memory error and how to resolve it, and how to troubleshoot performance bottlenecks in a distributed system. The second is data structures and algorithms: approaches to reversing a linked list, comparing sorting algorithms by time and space, and searching a large unsorted dataset. The third is Java data access: Spring JDBC versus Spring JPA, when to choose each in production, and managing database transactions under high concurrency.

Candidates also describe example scenarios such as walking a request from the browser to the database, optimizing a slow query in a Spring application, implementing a transformation or search on a given data structure, and refactoring a code snippet to run faster. Prepare each of these as a layered answer: the correct mechanism, its complexity or cost, the failure cases, and the trade-off against the alternative.

Candidates are told to expect follow-ups on each statement they make and to code standard data structure operations without library shortcuts. Treat every technology on your resume as something you will be asked to explain internally: how the JVM manages memory, what a Spring proxy does on a transactional method, what an index lookup costs. Frameworks alone do not carry the loop; candidates are warned that weak algorithm and data structure fundamentals will hurt.

The ArmorCode interview process

Candidates describe two stages, starting with a Technical Assessment and followed by Deep-Dive Interviews. The stages below are what candidates report, not a published process.

Technical Assessment

Candidates describe the Technical Assessment as the first stage, covering technical competency in algorithms and system design. Data structures and algorithms is the topic most associated with it. Because algorithms and design are both named, practise two kinds of answer: code you write and reason about aloud, and a structured talk-through of how components fit together under load. Coding questions are practised here as a type: linked-list operations, sorting trade-offs and search strategies. Work from the simplest correct version, state its complexity, then improve it and say what the improvement costs in memory or code.

What to demonstrate

  • Working code for classic data structure operations, with time and space complexity stated
  • Ability to break a problem into components and reason about scale and trade-offs
  • Clear spoken reasoning while solving

How to prepare

  • Write iterative, recursive and stack-based linked-list reversal from scratch, covering empty and single-node lists, and quote the complexity of each
  • Make a one-page table of insertion, merge, quick and heap sort and counting sort covering time, space, stability and worst case
  • Practise talking through a design for a service that ingests records from several external APIs, naming storage, queueing and failure handling
  • Rehearse the request path from browser to database so you can slot it into a design answer

Deep-Dive Interviews

Candidates describe the Deep-Dive Interviews as in-depth technical conversations that explore your previous work and your theoretical knowledge. The tip candidates pass along is that answers get followed up statement by statement, so a claim you make about a past project or a technology is a claim you may have to defend down to the internals. Pick two or three projects you can describe end to end, including the design alternatives you rejected, and review the theory behind every technology on your resume. Java memory behaviour, Spring data access and transaction handling are the areas the reported questions point to.

What to demonstrate

  • Depth of understanding of the work you list, not just its surface
  • Theoretical knowledge of runtime, networking and data access behaviour
  • Ability to justify engineering decisions and discuss their consequences

How to prepare

  • Write a one-page walkthrough of two past projects: problem, your design, the alternative you rejected, what went wrong, what you would change
  • For each technology on your resume, list three internals questions and answer them aloud, such as garbage collection phases or how a Spring transactional proxy works
  • Rehearse the Out of Memory and distributed performance answers as diagnostic sequences, not lists of causes
  • Practise answering the Spring JDBC versus JPA and transaction-handling questions with an example from code you have actually written

Where candidates lose points

Describing the browser request as just 'DNS, then the server responds'

Write the full chain: URL parsing, browser and OS cache checks, DNS resolution, TCP handshake, TLS handshake, HTTP request, load balancer or reverse proxy, application, database, response and rendering. Be ready to go one level deeper on any step.

Answering an Out of Memory question with 'increase the heap'

Separate the variants (Java heap space, GC overhead limit exceeded, Metaspace, unable to create native thread, direct buffer memory), then describe a diagnostic sequence: heap dump on error, dominator tree to find what retains memory, and then a fix for leak versus genuinely undersized heap versus unbounded input.

Giving a single linked-list reversal and stopping, or losing the next pointer mid-reversal

Present the iterative three-pointer version (O(n) time, O(1) space), the recursive version (O(n) stack depth), and a stack-based version (O(n) extra space). Trace the empty, one-node and two-node cases on paper before declaring it done.

Declaring Spring JPA better or JDBC faster without trade-offs

Compare on specific axes: control over SQL, mapping effort, N+1 queries and lazy loading, batch writes, dirty checking, and testability. Give a concrete choice rule, such as hand-written SQL for a reporting query and JPA for entity CRUD, and how you would verify the choice with query logs.

Claiming a technology on your resume and being unable to explain how it works underneath

Before the loop, list each technology you name and write answers to three internals questions about it. If you do not know something, say what you do know, reason from it, and state how you would confirm the rest.

Questions & practice

Can you explain the causes and potential resolutions for an Out of Memory Exception?

Medium · Technical Foundations and Web Mechanics

Approach

  1. Correct the name first: in Java it is java.lang.OutOfMemoryError, an Error rather than an Exception. Read the message, because each one points somewhere different: 'Java heap space', 'GC overhead limit exceeded' (parallel collector), 'Metaspace', 'unable to create new native thread', 'Direct buffer memory', 'Requested array size exceeds VM limit'.
  2. Group the causes. A leak means objects stay reachable: static collections, caches with no eviction, ThreadLocals on pooled threads, unregistered listeners, classloader leaks on redeploy. A working set that is genuinely too large means a whole result set loaded into memory or unbounded queues. An undersized -Xmx is the third case.
  3. Diagnose before changing the heap size: enable -XX:+HeapDumpOnOutOfMemoryError with -XX:HeapDumpPath, or take a dump with jcmd GC.heap_dump. Open it in Eclipse MAT and follow the dominator tree and retained sizes to the GC root holding the memory. GC logs (-Xlog:gc*) showing old-gen occupancy rising after each full GC indicate a leak.
  4. Fix the retention, then bound memory by design: caches with maximumSize or expiry (Caffeine), paginated or streamed DB reads (JDBC fetchSize, Spring Data Slice or Stream), bounded queues with back-pressure. Only then size the heap: -Xmx, or MaxRAMPercentage in containers.
  5. Edge cases: a container OOMKilled by the kernel is not a Java OOME (the process RSS crossed the cgroup limit, often through native or direct memory). Catching OutOfMemoryError and carrying on leaves the JVM in an unknown state.

Follow-up

  • How would you tell a memory leak apart from a heap that is simply too small for the load?
  • The pod is OOMKilled but no heap dump appears: where is the memory going?
  • Why is a ThreadLocal on a thread-pool thread a common leak, and how do you prevent it?

Describe multiple approaches to reversing a linked list.

Medium · Data Structures and Algorithms

Approach

  1. Iterative, the default answer: prev = null, curr = head; on each step save next = curr.next, set curr.next = prev, then advance prev = curr and curr = next; return prev. O(n) time, O(1) extra space.
  2. Recursive: if head is null or head.next is null, return head; otherwise newHead = reverse(head.next), then head.next.next = head and head.next = null; return newHead. O(n) time but O(n) call stack, and Java has no tail-call elimination, so a long list can throw StackOverflowError.
  3. Stack or array: push the nodes and relink them in pop order, or build a new list by prepending copies, which leaves the original intact. Both use O(n) extra space and are only worth it when the original must not be mutated.
  4. Edge cases to state and test: an empty list, a single node, two nodes. Make sure the old head's next ends up null, or the result contains a cycle. For a doubly linked list, swap prev and next on every node and return the last node.
  5. Know the variants: reversing positions left..right in one pass, reversing in groups of k, and reversing the second half to check a palindrome in O(1) extra space.

Follow-up

  • Reverse only the nodes between positions left and right in a single pass.
  • Reverse the list in groups of k, leaving any remainder shorter than k as it is.
  • Check whether the list is a palindrome using O(1) extra space.

Compare and contrast different sorting algorithms in terms of time and space complexity.

Medium · Data Structures and Algorithms

Approach

  1. Quadratic sorts: bubble, selection and insertion are O(n^2) worst case and in place. Insertion sort is stable, O(n) on nearly sorted input and fast for small n, which is why hybrid sorts use it for small runs. Selection sort makes the fewest swaps but is not stable.
  2. Merge sort is O(n log n) in every case and stable, but needs O(n) auxiliary memory for arrays. It suits linked lists (merging needs no extra array) and external sorting.
  3. Quicksort is O(n log n) on average and O(n^2) in the worst case with bad pivots (for example, sorted input with a first-element pivot). It sorts in place with O(log n) stack if you recurse on the smaller side, and it is not stable. Random or median-of-three pivots make the worst case unlikely.
  4. Heapsort is O(n log n) worst case with O(1) extra space, but it is not stable and its cache locality is poor. Comparison sorts have an Omega(n log n) lower bound. Counting sort O(n + k) and radix sort O(d(n + k)) beat it when keys are small integers or fixed width.
  5. Tie it to Java: Arrays.sort on primitives uses dual-pivot quicksort (stability does not matter for primitives), while object arrays and Collections.sort use TimSort, which is stable, O(n) on already sorted runs and O(n log n) worst case, with up to n/2 extra references.

Follow-up

  • How would you sort a 100 GB file with 4 GB of memory?
  • Why does stability matter when sorting records by several keys in turn?
  • Why does Java use different algorithms for primitive arrays and object arrays?

How would you efficiently search for an element in a large, unsorted dataset?

Medium · Data Structures and Algorithms

Approach

  1. Clarify first: one lookup or many, exact match or range, a static or changing dataset, and whether it fits in memory. The right answer depends on all four.
  2. For a single lookup, a linear scan in O(n) is optimal: without structure, every element may need checking. Sorting first costs O(n log n), so it only pays off across many queries.
  3. For repeated lookups, build an index once. A HashSet or HashMap costs O(n) to build and gives O(1) expected lookups, with no range queries. Sorting once and then binary searching gives O(log n) per query and supports ranges. A TreeMap supports ordered queries and updates in O(log n).
  4. To speed up a scan, split it into chunks across threads (ForkJoin or parallel streams) with a shared flag for early exit. The total work stays O(n), wall time drops to roughly O(n / p), and memory bandwidth becomes the limit.
  5. When the data does not fit: stream it sequentially from disk; build an on-disk index (a B-tree in a database, an inverted index for text); use a Bloom filter for fast 'definitely absent' answers (false positives possible, no false negatives); partition by key hash across machines and scatter-gather the query.

Follow-up

  • The data changes constantly while it is being queried: what structure would you keep, and what does each update cost?
  • How would you size a Bloom filter for a target false-positive rate?
  • The queries become range queries. What changes in your choice of structure?

How do you manage database transactions in a high-concurrency application?

Medium · Framework and Implementation Knowledge

Approach

  1. Keep transactions short and in the right place: @Transactional on service methods, no remote calls or user waits inside them, because each open transaction holds a pooled connection and its locks. Know the proxy rules: a self-invoked call skips the proxy, so @Transactional is ignored, and by default only unchecked exceptions trigger rollback (use rollbackFor for checked ones).
  2. Pick the isolation level from the anomalies you must prevent: dirty reads, non-repeatable reads, phantoms, lost updates, write skew. READ COMMITTED is the default in PostgreSQL and Oracle, REPEATABLE READ in MySQL InnoDB. PostgreSQL SERIALIZABLE uses SSI and aborts conflicting transactions with SQLSTATE 40001, which the caller must retry.
  3. Choose the concurrency control. For low contention, optimistic locking with @Version (catch OptimisticLockException and retry). For hot rows, pessimistic SELECT ... FOR UPDATE via @Lock(PESSIMISTIC_WRITE). Best of all, an atomic conditional update: UPDATE stock SET qty = qty - ? WHERE id = ? AND qty >= ?, then check the affected-row count.
  4. Handle deadlocks and waits: acquire locks in a consistent order, keep lock scope small, set lock and statement timeouts, and retry deadlock victims with backoff. Use FOR UPDATE SKIP LOCKED for worker queues so consumers do not block each other.
  5. Across service boundaries, avoid distributed transactions: use the transactional outbox pattern, idempotency keys for retried requests, and sagas with compensating actions. Size the connection pool (HikariCP) against transaction duration, not thread count.

Follow-up

  • Two requests decrement the same inventory row at READ COMMITTED with read-then-write code. Walk through the lost update and fix it.
  • When would you choose optimistic over pessimistic locking, and what does each cost under heavy contention?
  • A @Transactional method is called from another method in the same class and nothing rolls back. Why?

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?

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?

What sequence of events occurs when a user types a URL into a web browser?

Medium · Technical Foundations and Web Mechanics

Approach

  1. Walk the request in order: the browser parses the URL, checks its HSTS list to upgrade http to https, then checks the HTTP cache and any service worker before touching the network.
  2. Resolve the host: browser DNS cache, OS cache and hosts file, then the configured recursive resolver. Then open the connection: a TCP three-way handshake, or QUIC over UDP for HTTP/3.
  3. TLS 1.3 handshake in one round trip: ClientHello carries SNI and ALPN (which picks h2 or http/1.1), the server sends its certificate chain, and the browser validates the chain, the hostname and the expiry before any HTTP bytes flow.
  4. Server side: the request carries Host, cookies and cache validators, and passes through a CDN or load balancer and a reverse proxy to the app server and database. The response comes back with a status (including 301/302 redirects that restart the flow), headers such as Cache-Control and Set-Cookie, and a body.
  5. Rendering: HTML is parsed into the DOM and CSS into the CSSOM, they combine into a render tree, then layout, paint and composite. Scripts without async or defer block parsing, and subresources reuse the connection (keep-alive, or HTTP/2 multiplexing).

Follow-up

  • What changes in this sequence with HTTP/3 and QUIC, and which round trips disappear?
  • Where would you add caching to cut the latency of the first visit versus a repeat visit?
  • How does HSTS stop the very first plain-HTTP request from being intercepted, and what does preloading add?

How does the DNS lookup process work in detail?

Medium · Technical Foundations and Web Mechanics

Approach

  1. Start at the stub resolver: browser cache, OS cache and hosts file, then a recursive query to the configured resolver over UDP port 53. It falls back to TCP when a response is truncated, and DoH or DoT apply when they are configured.
  2. The recursive resolver then queries iteratively. It asks a root server (known from root hints), gets a referral to the TLD name servers with glue records, asks the TLD and gets a referral to the domain's authoritative servers, then gets the A or AAAA answer, following any CNAME chain.
  3. Every answer is cached for its TTL at each layer, and NXDOMAIN is negatively cached using the SOA values. Explain the difference: the client sends a recursive query, and the resolver does the iterative work.
  4. Name the record types and their rules: A, AAAA, CNAME, NS, MX, TXT, SOA. A CNAME cannot sit beside other records at the same name, which is why the zone apex cannot be a plain CNAME.
  5. Security: random transaction IDs and source ports make cache poisoning harder; DNSSEC (RRSIG, DNSKEY, DS chain) proves an answer is authentic but does not encrypt it; DoH and DoT encrypt the transport.

Follow-up

  • Why does a DNS change take time to reach everyone, and how would you lower the TTL ahead of a migration?
  • How does DNS-based load balancing or geo-routing work, and why do client and resolver caching limit it?
  • When does DNS switch from UDP to TCP?

What are the key differences between Spring JDBC and Spring JPA?

Medium · Framework and Implementation Knowledge

Approach

  1. Spring JDBC (JdbcTemplate, NamedParameterJdbcTemplate, and JdbcClient in Spring 6.1+): you write the SQL and map rows with a RowMapper. Spring handles getting and releasing connections, PreparedStatement binding, and translating SQLException into the DataAccessException hierarchy. There is no entity state, lazy loading or caching.
  2. Spring Data JPA: repository interfaces on top of a JPA provider, usually Hibernate. Entities are mapped with annotations, and a persistence context gives a first-level cache and dirty checking, so changes are flushed automatically at commit. Queries come from method names, JPQL, the Criteria API or native SQL.
  3. The trade-off: JPA removes CRUD boilerplate and manages object graphs, cascades and optimistic locking (@Version), but it hides the SQL. That causes N+1 selects from lazy associations (fix with JOIN FETCH, @EntityGraph or batch fetching), LazyInitializationException outside the session, and surprises in flush ordering. JDBC gives exact, predictable SQL and simple batching through batchUpdate.
  4. Transactions: both work with @Transactional, JDBC through DataSourceTransactionManager and JPA through JpaTransactionManager, which can also expose the same transaction to JdbcTemplate on the same DataSource.
  5. Mention the third option: Spring Data JDBC is aggregate-oriented, with repositories but no lazy loading and no dirty checking. It sits between the two.

Follow-up

  • How would you detect an N+1 query problem in a running service and fix it?
  • If JdbcTemplate and a JPA repository run in the same transaction, why might the JDBC query miss an entity change, and how do you fix it?
  • What is the difference between persist and merge, and what does save() call for a new versus an existing entity?

When would you choose one data access strategy over the other in a production environment?

Medium · Framework and Implementation Knowledge

Approach

  1. Use JPA for domain-heavy CRUD: transactions that load an aggregate, change a few fields and commit, where dirty checking, cascades and @Version optimistic locking save code and prevent lost updates.
  2. Use JDBC for reporting and analytical reads, bulk loads and updates (batchUpdate, plus driver settings such as reWriteBatchedInserts on the PostgreSQL driver or rewriteBatchedStatements on MySQL), vendor-specific SQL (window functions, CTEs, upserts with ON CONFLICT), and hot paths where every statement must be predictable.
  3. A mixed approach is common and defensible: JPA on the write side, and JdbcTemplate or DTO projections on read-heavy paths. Mark read paths @Transactional(readOnly = true); since Spring 5.1 this sets Hibernate's flush mode to MANUAL and skips dirty checking.
  4. Know the JPA bulk traps. With IDENTITY id generation, Hibernate cannot batch inserts; a sequence with hibernate.jdbc.batch_size enables batching. JPQL bulk UPDATE or DELETE bypasses the persistence context and leaves loaded entities stale. Clear the context periodically when processing large batches.
  5. Decide from evidence: log the SQL and Hibernate statistics, count queries per request, compare p95 latency before and after, and move only the measured slow paths to JDBC rather than rewriting everything.

Follow-up

  • You need to insert a million rows nightly. How would you do it with each approach, and what settings matter?
  • Why does IDENTITY key generation disable Hibernate insert batching?
  • A single JPA endpoint is slow under load. How would you migrate just that path without destabilising the rest?

How would you troubleshoot performance bottlenecks in a distributed system?

Medium · Technical Foundations and Web Mechanics

Approach

  1. Pin the symptom down with numbers: which endpoint, p50 versus p99 latency, throughput, error rate, and when it started. Then line it up with deploys, config changes and traffic shifts before forming a theory.
  2. Find the slow hop with distributed tracing (propagated trace context, OpenTelemetry spans) to see which service dominates the critical path. Use RED metrics (rate, errors, duration) per service and USE (utilization, saturation, errors) per resource: CPU, memory, disk, network, thread pools and connection pools.
  3. Check the usual culprits: slow queries (EXPLAIN ANALYZE, missing index, N+1 selects from an ORM), connection pool exhaustion (threads waiting on HikariCP), lock contention, GC pauses, downstream calls made sequentially that could run in parallel, hot partitions or keys, queue backlogs, and retries amplifying load.
  4. Confirm on the suspect process with a profiler (an async-profiler flame graph) and thread dumps (jstack, jcmd Thread.print) that show threads BLOCKED or WAITING. Reproduce under a load test, change one thing at a time, and verify against the baseline metric.
  5. Explain tail latency: fanning out to N services means the slowest one sets the request's latency. Mitigations are per-call timeouts inside an overall budget, parallel independent calls, caching, bulkheads, and retries with backoff, jitter and a cap.

Follow-up

  • p99 doubled while p50 is flat: what does that point to, and what do you check first?
  • Traces show the time is spent in database calls, yet database CPU is low. What next?
  • How do you stop retries from turning a slow dependency into an outage?

Suggested practice checklist

The plan walks the ten reported questions in the order of the loop's fundamentals, then closes with the previous-work conversation candidates describe for the second round. Each day ends with a written artifact you can reread before the interview.

Day 1 — Browser request and DNS, end to end

  • Write the full path from typing a URL to a rendered page as numbered steps: URL parsing, cache and HSTS checks, DNS, TCP and TLS handshakes, HTTP request, proxy or load balancer, application, database, response
  • Write the DNS lookup in detail: stub resolver, recursive resolver, root, TLD and authoritative servers, A, AAAA, CNAME and NS records, TTL caching and negative caching
  • Say both answers aloud without notes and fix the steps you skipped

Deliverable: A one-page request lifecycle and a one-page DNS resolution diagram, each with the steps you missed on the first spoken attempt marked.

Day 2 — Out of Memory errors and distributed performance debugging

  • Make a table of OutOfMemoryError variants (heap space, GC overhead limit, Metaspace, native thread, direct buffer) with cause and fix for each
  • Write a small Java program with a leak, such as a static map that keeps growing, run it with a heap dump on error enabled, and find the retaining path in a heap analyzer
  • Outline a troubleshooting sequence for a slow distributed request: latency percentiles, tracing across services, saturation of CPU, threads, connection pools and queues, slow downstream calls, GC pauses

Deliverable: The OOM variants table, a written heap-dump finding for your leak program, and a numbered troubleshooting runbook for a slow multi-service request.

Day 3 — Linked-list reversal and large-dataset search

  • Code linked-list reversal three ways: iterative with prev, curr and next, recursive, and with a stack. Run each on empty, one-node and two-node lists
  • Write the complexity of each version and one sentence on when you would pick it
  • Write the search options for a large unsorted dataset: linear scan, building a hash set for repeated lookups, sorting then binary search, an index, partitioning or parallel scan, with the break-even for each

Deliverable: Three working reversal implementations with test cases, and a one-page decision table for searching unsorted data.

Day 4 — Sorting trade-offs and optimizing a snippet

  • Implement merge sort and quicksort from scratch without library calls, then tabulate time, extra space, stability and worst case for insertion, merge, quick, heap and counting sort
  • Write down what Java uses for primitives versus objects and why stability matters for the second case
  • Take a nested-loop snippet and refactor it to a hash-based or sorted approach, stating the new complexity and the memory cost, and implement an iterative tree traversal

Deliverable: Two working sort implementations, a comparison table, and one before/after refactor with complexity written for both.

Day 5 — Spring JDBC versus Spring JPA and SQL tuning

  • Write a side-by-side of JdbcTemplate and Spring Data JPA covering control over SQL, mapping effort, N+1 queries, lazy loading, batching and dirty checking
  • Write a decision rule for production with two examples where each wins

Deliverable: A JDBC versus JPA comparison with a decision rule, plus a worked slow-query note showing the plan before and after your index.

Day 6 — Transactions under high concurrency

  • Write how a Spring transactional method works through a proxy, including why a self-invoked call bypasses it and which exceptions trigger rollback by default
  • List isolation levels and the anomalies each prevents, then contrast optimistic locking with a version column and retry against pessimistic locking with SELECT ... FOR UPDATE
  • Write a short answer on keeping transactions short, ordering locks to avoid deadlocks, sizing connection pools, and making retried writes idempotent

Deliverable: A one-page transaction-handling answer with a concrete example of a lost update and the fix, written the way you would say it aloud.

Day 7 — Deep-dive rehearsal on your previous work

  • Pick two projects and write a design walkthrough for each, including the rejected alternative and one thing that failed
  • Do a mock where a partner follows up on every sentence of your walkthrough, and write down every question you could not answer
  • Re-answer aloud the one reported question you feel weakest on

Deliverable: Two project walkthrough pages, a list of unanswered follow-ups with answers filled in, and two behavioral stories outlined in situation, action, result form.

Behavioural preparation

No separate behavioral round is reported, but candidates describe the Deep-Dive Interviews as exploring your previous work, so project stories carry real weight. Prepare stories where you can name the design, the alternative you rejected, the failure and the result, and keep each one tied to Java, data access, performance or integration work so it survives repeated follow-up questions.

  1. Walk through the project on your resume that you know best: the design you chose, the alternative you rejected, and what you would change now.
  2. Describe a production performance problem you diagnosed: what you measured first, what you ruled out, and what fixed it.
  3. Describe a time you chose a data access approach, such as an ORM, hand-written SQL or a cache, and later had to revise it. What forced the change?
  4. Tell the story of an integration with an external API or system: how you handled inconsistent data formats and partial failures.
  5. Describe a code review where you disagreed with the author. What did you say, and how did it end?

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?

Argue against a design, lose, and commit anyway

Medium · disagreement · service boundaries · decision records

Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.

Approach

  1. State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
  2. Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
  3. Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
  4. Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.

Follow-up

  • What threshold on that alert would have proved you right, and did anyone ever look at it?
  • If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?