Interview guides · ATS
ATS Software Engineer Interview Guide
ATS Software Engineer interview guide: Java, OOP and handwritten coding topics candidates report, five reported questions with guidance and a 7-day plan.
The 60-second brief
Candidate reports do not say what ATS sells or which product a given team owns, so this guide does not guess. What they do describe is the engineering job: designing, building and maintaining software that supports the business, turning requirements from product managers and other stakeholders into working code, taking part in design discussions, writing technical documentation and reviewing teammates' code. They also mention work on new features, on improving existing frameworks and on paying down technical debt. Ask your recruiter which team you would join and what it ships, because that decides how much of your Java depth the day job uses.
This guide is for candidates interviewing for a Software Engineer role at ATS. It covers the topics candidates report (Java, object-oriented programming, handwritten coding and general technical breadth), the five reported questions, the behavioral material around performance work and past projects, and a seven-day schedule. It does not cover other roles, and it does not claim a stage order that nobody has reported.
Candidates report a process that can run over a few days and mixes technical assessments, including written coding in some loops, with conversations about past projects and how you approach problems. No stage names or order have been reported.
Focus areas
- Java
- Object-Oriented Programming (OOP)
- Coding by Hand (written coding)
Overview
The reported topics are narrow and fundamental: Java, object-oriented programming, coding by hand, and breadth across the basics. Candidates also describe being surprised by how much weight basic subjects get, so the efficient preparation is depth on the core language and the standard library rather than exotic frameworks. The example scenarios candidates give are explaining the difference between an interface and an abstract class in Java, and describing how you handle exceptions in a production environment. Both reward a concrete use case and a trade-off, not a recited definition.
Five questions are reported. Three are technical fundamentals: explaining core Java concepts, debugging complex code under pressure, and keeping code maintainable and scalable for later changes. One is about designing a solution to a technical problem from scratch, and one asks for a story about optimizing software for better performance. They are open-ended, so prepare a method for each: a debugging sequence you can narrate, a design routine you follow on any problem, and a measured performance story with before and after numbers.
The written-coding format is the part most worth rehearsing physically. Candidates are advised to practise coding by hand or on a whiteboard, to keep names and structure clean even in a test, to talk through their reasoning while they work, and to say so if they are stuck rather than going quiet. Resume depth also matters: be ready to discuss every project you list, including your own role and what you would change.
The requirements candidates describe list Java proficiency, solid data structures and clean, documented code as must-haves, with agile experience, cloud infrastructure familiarity and work on large collaborative projects as extras. Use the plan below to spend most of your time on those must-haves and a smaller share on the extras, mainly so you can talk about them honestly.
Where to focus your preparation
No stage names or order are reported for this loop, so it appears here as a single preparation focus built from what candidates describe. Confirm the actual sequence with your recruiter.
Preparation focus
Candidates describe a process that can span a few days and combines technical assessments, which may include written coding, with discussions of past projects and of how you solve problems. The reported topics are Java, object-oriented programming, coding by hand and broad technical fundamentals. Because the stages are not named, prepare for all of those formats and ask your recruiter whether you will write code on paper, on a whiteboard or on a computer. Work in this order: language and OOP fundamentals first, then handwritten problems where you narrate your reasoning, then your project and performance stories.
What to demonstrate
- Java fundamentals and standard-library knowledge, including explaining the reasoning behind your choices
- Object-oriented design judgment, such as interface versus abstract class
- Writing readable, correct code by hand, with clear naming and structure
- Describing your own projects and problem-solving method
How to prepare
- Write a one-page Java sheet in your own words covering interface versus abstract class, equals and hashCode, collections choice, exception handling, and memory and concurrency basics, with one code example each
- Solve three array, string and hash-map problems on paper, then type them up and check whether they compile and pass your own edge cases
- Rehearse aloud the debugging and from-scratch design questions, naming each step you take
- Prepare a two-minute account of every project on your resume: your role, one design decision and one thing you would change
Where candidates lose points
Answering the interface versus abstract class question with two definitions and no decision rule
Give the rule you actually use: an interface for a capability several unrelated classes can implement, an abstract class for shared state and partial implementation. Mention default methods and single inheritance of classes, then add a short example from your own code.
Describing exception handling as catching Exception and logging it
Separate checked from unchecked exceptions, never swallow an error, keep the original cause when you wrap it, use try-with-resources for closeable resources, and log once at a boundary with enough context to reproduce the failure.
Writing a handwritten solution in silence, then discovering a bug at the end
Restate the problem, name the brute-force approach and your target, outline the steps, and narrate while you write. Finish by tracing a small input through your code and checking empty, null, single-element and off-by-one cases.
Giving a performance story with no measurement, so the claim of improvement cannot be checked
Before the interview, write down the before figure, the after figure, the tool you used to find the bottleneck, and the cost of the change. If you lack exact numbers, use the order of magnitude and say how you measured it.
Being unable to explain a project listed on your resume beyond its name
For every project, prepare your role, the main design decision and its alternatives, one problem you hit and one thing you would change. Drop anything you cannot discuss in depth.
Questions & practice
Walk me through your process for designing a solution to a technical problem from scratch.
Medium · Problem-Solving and Coding
Approach
- Start by clarifying the problem: the input type and size, the value ranges, whether duplicates or nulls can appear, and exactly what to return. Then work through one small example by hand to confirm you understood it correctly.
- State the brute-force solution and its complexity first. Then find the repeated work it does and pick the technique that removes it: hashing for lookups, sorting plus two pointers, a sliding window for contiguous ranges, or dynamic programming for overlapping subproblems.
- Show the trade-off with a familiar example. For pair-sum, checking every pair is O(n^2). A HashMap pass is O(n) time with O(n) extra space. Sorting plus two pointers is O(n log n) with little extra space, but it loses the original indices unless you carry them along.
- Candidates report written coding tasks, so practise writing the solution out with clear names and small helper methods. Then trace your own example through the code by hand.
- Test the edge cases before you call it finished: empty input, a single element, all duplicates, negative numbers and overflow in an int sum. End by stating the final time and space complexity.
Follow-up
- Your solution uses O(n) extra space. Can you do it in constant space, and what does that cost?
- Which test cases would you write first, and why?
- How would the design change if the input arrived as a stream instead of an array?
Track a rolling failure rate per destination for circuit decisions
Easy · sliding window · ring buffer · circuit breaker
The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.
Approach
- Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
- Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
- State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
- Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.
Follow-up
- The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
- A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?
Diff a projection against the primary without per-row point reads
Hard · reconciliation · range hashing · throttling
The listing projection has drifted and some rows show a stale version. The primary holds 40,000,000 resource rows across 12,000 tenants while serving 1,200 writes and 14,000 reads per second. The obvious repair, reading each resource row and comparing its version against the projection, is correct and would eventually finish. Explain precisely why it is unacceptable here, then give a diff that finds the differing rows, state its complexity, and make it safe to run against a live primary. Replication lag is usually under 100 ms and is not bounded.
Approach
- Quantify the naive cost rather than calling it slow: 40,000,000 point reads at even 0.5 ms each is over five hours serialised, and the only lever is concurrency, which is exactly what you cannot spend. The primary's pool is sized for the write path, and 40,000,000 random reads evict the buffer cache that sustains the 85 percent cache hit rate, so the audit degrades the system it is auditing.
- Replace random access with one ordered pass per side. Both sides can be read in (tenant_id, resource_id) order, which is a sequential scan on each and a merge join in O(n) time and O(1) memory. For a dense diff that is the whole answer, and it reads the primary once instead of 40,000,000 times.
- For the expected sparse case, compare range hashes instead of rows: partition the key space, compute per range an order-independent aggregate over hash(resource_id, version), compare aggregates, and descend only into ranges that differ. With d differing rows and branching factor B, at most d ranges mismatch per level, so the drill-down examines O(d log_B(n/d)) ranges and reads full rows only in mismatching leaves.
- Aggregate with a sum modulo 2^64 or a multiset hash, never XOR. XOR is order-independent but self-cancelling, so two rows wrong in the same way, or a row duplicated on one side, leave the range aggregate matching and the range is declared clean.
Follow-up
- The diff reports 900 stale rows. How do you decide between patching those rows and rebuilding the projection from resource_revision?
- Same job, but the projection lives in a search index that cannot be scanned in key order. What changes?
Explain why the owner filter ignores the listing index
Medium · composite index · query plans · sort elimination
The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.
Approach
- Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
- For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
- Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
- Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.
Follow-up
- 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
- A dashboard runs this for 40 owners in one page load. What changes about the design?
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
- 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.
- 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.
- 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.
- 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?
Can you explain the core concepts of Java and how they apply to our current projects?
Medium · Technical Fundamentals
Approach
- Since you cannot know the team's current codebase, ask which part of the stack they mean. Then tie each concept to a project of your own instead of reciting a list.
- Cover the OOP pillars with Java specifics. Encapsulation means private fields behind methods. Overloading is resolved at compile time, while overriding uses runtime dynamic dispatch. A class extends one class but can implement many interfaces. An abstract class can hold state and constructors; an interface, since Java 8, adds default methods but no instance state.
- Explain the JVM model: source compiles to bytecode, which the JIT compiles further at run time. Objects live on the heap and local variables and frames on the stack. Garbage collection is generational (G1 has been the default since JDK 9). Java always passes by value, and for objects the value passed is the reference.
- Cover the collections and the equals/hashCode contract. HashMap get and put are O(1) on average. Since Java 8, a bucket becomes a tree once it reaches 8 entries and the table has at least 64 slots, so the worst case is O(log n). Contrast ArrayList with LinkedList, and explain why String is immutable and when to use StringBuilder.
- Finish with exceptions and concurrency. Checked exceptions must be declared or caught; unchecked ones extend RuntimeException. try-with-resources closes any AutoCloseable. volatile guarantees visibility but makes compound operations like count++ atomic only if you use AtomicInteger or synchronized. For shared maps, use ConcurrentHashMap.
Follow-up
- What happens if you override equals but not hashCode and then use the object as a HashMap key?
- Now that interfaces can have default methods, when would you still choose an abstract class?
- What does volatile guarantee, and what does it not guarantee?
How do you ensure your code is maintainable and scalable for future updates?
Medium · Technical Fundamentals
Approach
- Keep each class to one responsibility and depend on interfaces, with implementations passed in through constructors. That lets you swap a storage or client implementation without touching its callers. Prefer composition to deep inheritance trees.
- Rely on tests to make change safe. Put unit tests on the public behaviour of each class rather than its internals, so you can refactor without rewriting the tests. Pair them with code review and consistent naming, and use Javadoc on public APIs to explain why the code works the way it does.
- Scaling in the code itself means three things. Choose data structures by complexity, for example a HashMap lookup instead of an O(n) list scan inside a loop. Avoid N+1 database queries. Keep services stateless or make shared mutable state thread-safe, so the code can run on several instances.
- Name the trade-off: every abstraction adds indirection. Add extension points only where change is likely or has already happened twice, and say how you decide that instead of abstracting everything in advance.
- Back the answer with one concrete example from your own work: the code before, the specific change (an extracted interface, a removed duplicate, an added test), and what it made easier afterwards.
Follow-up
- How do you decide that an abstraction is premature?
- What has to change in this code before it can run on several instances behind a load balancer?
- Tell me about a refactor you made without breaking any callers. How did you know it was safe?
How do you approach debugging a complex piece of code under pressure?
Medium · Technical Fundamentals
Approach
- Contain the damage before looking for the cause. If users are affected, roll back or turn off the feature flag first, then debug without the clock running. Say out loud that you are separating mitigation from root cause.
- Reproduce the failure reliably and shrink it to the smallest input that still fails. Read the whole Java stack trace, including every 'Caused by' section, because the top frame is often only where the failure surfaced.
- Form one hypothesis at a time and test it with something that rules out half the possibilities. Use a conditional breakpoint, targeted logging, or git bisect between a known-good and a known-bad commit, which needs about log2(n) builds for n commits.
- For failures that a debugger makes disappear (often races), use evidence that does not change the timing. Take thread dumps with jstack to find deadlocks, a heap dump for OutOfMemoryError, and timestamps in the logs rather than breakpoints.
- Before you fix the bug, write a unit test that fails because of it. Keep that test as a regression guard, and give a short status update covering what you know, what you have ruled out, and what you will check next.
Follow-up
- The bug shows up only in production and never locally. What do you check first?
- How do you tell the change that triggered a failure apart from its actual cause?
- When do you roll back, and when do you fix forward?
Suggested practice checklist
The plan starts by confirming the format, then spends the middle days on Java, OOP and written coding, the three topics candidates report most, and finishes with debugging, the performance story and a mixed mock.
Day 1 — Confirm the format and inventory your projects
- Write the questions to send your recruiter: the order of stages, whether you will write code by hand or on a computer, which language is allowed, and which team the role sits on.
- List every project on your resume with your role, the main technical decision and one thing you would change.
Deliverable: A list of recruiter questions and a one-page project inventory.
Day 2 — Core Java fundamentals
- Write a one-page sheet in your own words on interface versus abstract class, equals and hashCode, String immutability, collection choice, checked versus unchecked exceptions, and heap, stack and garbage collection.
- Write a short compiling example for each item, then explain the sheet aloud as if answering a general question about core Java concepts.
Deliverable: A Java sheet with one working example per concept and a recorded spoken explanation.
Day 3 — Object-oriented design and maintainability
- Take a class you wrote that grew messy and refactor it on paper: single responsibility, composition over inheritance where it fits, and an interface where several implementations are plausible.
- Write your answer to how you keep code maintainable and scalable for later changes, with three concrete practices from your own work such as tests, documentation, code review and clear module boundaries.
Deliverable: A before and after sketch of one refactored class and a written answer to the maintainability question.
Day 4 — Coding by hand
- Solve three array, string and hash-map problems on paper, narrating aloud, then type them in and fix whatever fails to compile.
- Practise the from-scratch routine: restate the problem, give the brute-force version, choose a data structure, state complexity, then code.
Deliverable: Three handwritten solutions with your corrections marked, and a one-line statement of the design routine you will use.
Day 5 — Debugging and performance
- Write your debugging sequence for a complex failure: reproduce, narrow the scope, form one hypothesis at a time, check logs and recent changes, fix, add a test.
- Rehearse it aloud against a bug you really fixed, including how you stayed methodical under pressure.
Deliverable: A written debugging checklist applied to one real bug, plus notes on both SQL performance drills.
Day 6 — Performance story and behavioral prompts
- Write your optimization story with the before figure, the profiling method, the change, the rejected alternatives, the after figure and the cost.
Deliverable: A one-page optimization story with numbers and two rehearsed behavioral answers.
Day 7 — Mixed mock with pen and paper
- Run a full mock using only paper: answer the core Java, debugging, maintainability and from-scratch design questions aloud, then solve one coding problem while narrating.
- Write the questions you want to ask about the team's projects, how agile practice works there, and any cloud infrastructure involved.
Deliverable: A completed paper mock with a list of weak spots and your questions for the interviewers.
Performance story and project walkthrough
Candidates report discussions of past projects and problem-solving method alongside the technical assessments, and one reported question asks for a performance-optimization story. Prepare stories where you made a decision, measured the result and can name the trade-off, and be ready to go deep on anything listed on your resume.
- Walk me through the project on your resume you know best: your role, the main design decision, and what you would change.
- Tell me about a time you asked for clarification before starting work, and how the answer changed what you built.
- Describe a time you were stuck on a bug under deadline pressure and how you worked through it.
- Tell me about feedback you received in a code review and what you changed.
Describe a time you had to optimize a piece of software for better performance.
Medium · Technical Fundamentals
Approach
- Choose one case where you owned the change, with a symptom you can put a number on: a slow endpoint, a batch job that overran, a memory-heavy service. Open with the before figure and the workload it was measured on, not with the fix.
- Show how you found the bottleneck before changing anything: a profiler or Java Flight Recorder capture, GC logs, a query plan, or timing around suspected hot spots. Say what you ruled out and why the evidence pointed where it did.
- Describe the change and the alternatives you rejected. Typical examples are replacing a nested-loop lookup with a HashMap, batching an N+1 query pattern, narrowing a synchronized block, or caching with a defined invalidation rule. Name the trade-off you accepted, such as memory, added complexity or staleness.
- Close with proof: the same workload re-measured, the after figure, and the tests that showed behaviour was unchanged. Say what you monitored afterwards. A story that says it got faster without a number is the most common way this answer falls flat.
- Keep it in first person singular and keep the team in the background: what you decided, what you measured and what you would do differently.
Follow-up
- How did you know that was the bottleneck and not just the most obvious suspect?
- What did the optimization cost in readability, memory or flexibility, and would you accept that cost again?
- How did you confirm the output stayed correct, and how would you catch a regression later?
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
- 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.
- 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.
- 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.
- 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?
Reverse your own decision and price the reversal
Medium · reversibility · measurement · migrations
Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.
Approach
- State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
- Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
- Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
- Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.
Follow-up
- What in that decision was irreversible, and did you know it was irreversible when you made it?
- How did you tell the people who had already built on top of the original decision?