ThoughtWorks Interview Questions · 2026

ThoughtWorks Interview Questions (2026), Real, With Answers

A candidate who'd cleared four rounds at a big product company in six weeks once told me the ThoughtWorks loop was the first one that made him nervous about a laptop he never had to touch alone. There's no algorithm quiz on a shared doc. Instead there's a real editor, and a consultant sitting next to him for close to two hours asking him to write tests first and explain every decision out loud as he types. ThoughtWorks describes its own process as "collaborative" on its careers site, built around a recruiter conversation, a technical stage that can include pair programming, case studies, and design challenges, and a separate conversation about leadership and culture fit (ThoughtWorks Careers). For entry-level graduate hires through the STEP program, that technical stage is preceded by an aptitude MCQ round and a shorter phone screen before the onsite.

Candidates on Indeed rate the overall difficulty around 6 out of 10 and the timeline close to a month end to end, with a chunk reporting it stretches past that (Indeed, ThoughtWorks interview reviews). What makes the loop distinct isn't difficulty in the LeetCode sense. It's that the pairing round splits into two separate sessions, often with two different consultants, specifically so nobody's day gets defined by one person's read of a candidate. One session usually leans toward a coding kata done test-first; the other leans toward reading and improving code that's already there, closer to what a real engagement with a client looks like in week one.

This page collects the actual questions candidates report being asked across that loop: the MCQ-style aptitude questions from the graduate screen, DSA problems from technical phone screens, the kata-style pairing problems ThoughtWorks is known for (Bowling Game, Mars Rover, Conference Track Scheduler, and the String Calculator kata originated by Roy Osherove and widely used across TDD-focused pairing rounds, per Coding Dojo's kata catalog), CS fundamentals, low-level design prompts, and the values-driven behavioral questions the culture round is known for. Every question below is one you could actually be asked out loud. None of them are questions about the format of the interview itself.

50Questions
4Rounds
Pair ProgrammingFormat
~1 MonthTimeline

Online assessment: aptitude and SQL screen

Graduate and campus hires usually see this first at ThoughtWorks, before any human enters the loop. It's short, timed, and closer to a logic test than a programming exam.

Easy questions

19

This is 60 divided by 0.75 hours, which comes out to 80 km/h. The trap isn't the arithmetic, it's converting 45 minutes to 0.75 hours before dividing rather than dividing by 45 and fixing units afterward. Candidates who set up the unit conversion first almost never get this wrong; candidates who try to do it in their head backwards from km/h sometimes do.

Interviewers use a handful of these early because a candidate who visibly slows down and writes out units, rather than guessing at a formula, tends to carry that same discipline into the pairing session later that day.

The differences between consecutive terms are 4, 6, 8, 10, which themselves increase by 2 each time. The next difference is 12, so the next term is 30 + 12 = 42. Another way to see it: each term is n(n+1), so the sixth term is 6 x 7 = 42.

Both routes get the same answer, and either is a fine thing to say out loud in a ThoughtWorks screen. What the screen is actually checking is whether you can spot a second-order pattern at all, not which method you use to get there.

Take a cost price of 100. Marked price after a 25% markup is 125. A 10% discount on 125 is 12.5, so the selling price is 112.5. Net profit is 12.5 on a cost of 100, which is a 12.5% profit.

The common mistake is subtracting the two percentages directly, 25% minus 10%, and answering 15%. Markup and discount apply to different base amounts, the cost price and the marked price respectively, so they don't combine by simple subtraction.

Walk the list once, keeping track of the previous node, and flip each node's next pointer to point backward instead of forward.

java
Node reverse(Node head) {
    Node prev = null;
    Node curr = head;
    while (curr != null) {
        Node next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    return prev;
}

The follow-up a ThoughtWorks interviewer usually has ready: can you do it recursively, and what's the tradeoff? Recursive reversal is a few lines shorter but costs O(n) stack space instead of O(1), which matters on a long list where the iterative version is the safer default.

The brute-force nested loop is O(n squared) and is the wrong answer here on purpose, it exists so you can name the tradeoff before improving it. A single pass with a hash map gets you O(n) time and O(n) space: for each number, check whether target minus that number has already been seen.

python
def two_sum(nums, target):
    seen = {}
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:
            return [seen[complement], i]
        seen[n] = i
    return []

Count every character's frequency in one pass, then walk the string a second time and return the first character whose count is exactly one.

python
def first_unique(s):
    counts = {}
    for c in s:
        counts[c] = counts.get(c, 0) + 1
    for c in s:
        if counts[c] == 1:
            return c
    return None

Two passes, each O(n), which is worth stating explicitly since some candidates assume a two-pass solution is automatically worse than a one-pass one. It isn't here, both are linear, and the two-pass version is easier to read correctly on the first try under time pressure.

Use two pointers starting from both ends, skipping any character that isn't alphanumeric, and compare the remaining characters case-insensitively as the pointers move toward the middle.

python
def is_palindrome(s):
    left, right = 0, len(s) - 1
    while left < right:
        while left < right and not s[left].isalnum():
            left += 1
        while left < right and not s[right].isalnum():
            right -= 1
        if s[left].lower() != s[right].lower():
            return False
        left += 1
        right -= 1
    return True

The depth of an empty tree is zero. Otherwise it's one plus the larger of the depths of the left and right subtrees, which is a direct recursive definition.

java
int maxDepth(Node root) {
    if (root == null) return 0;
    return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}

Interviewers sometimes ask you to convert this to an iterative version using a queue or stack right after you finish the recursive one, mostly to see whether you understand why the recursive version works rather than having memorized it.

Sort both strings and compare, which is O(n log n), or count character frequencies in both and compare the counts, which is O(n). The frequency-count version is the one worth leading with once asked to optimize.

python
from collections import Counter

def is_anagram(a, b):
    return Counter(a) == Counter(b)

The base version is trivial and isn't really the point. The extension is: instead of hardcoding a chain of if/elif checks, store the rules as a list of (divisor, word) pairs and build the output by checking each rule in order.

python
def fizzbuzz(n, rules=((3, "Fizz"), (5, "Buzz"))):
    result = []
    for i in range(1, n + 1):
        output = "".join(word for divisor, word in rules if i % divisor == 0)
        result.append(output or str(i))
    return result

What the pairing partner is actually evaluating is whether you notice the hardcoded version doesn't scale the moment a third or fourth rule gets added, and whether you refactor toward the data-driven version on your own before being asked to.

This is Roy Osherove's well-known TDD kata, and the point of the exercise is the order you build it in, not the final code. You're expected to write the smallest possible failing test first, empty string returns 0, make it pass, then add one test at a time: single number, two numbers, newline as a delimiter, and only then generalize.

python
def add(numbers):
    if numbers == "":
        return 0
    normalized = numbers.replace("\n", ",")
    return sum(int(n) for n in normalized.split(","))

Pairing partners notice candidates who write the fully general solution on the first line and then retrofit tests to match it. That inverts the exercise. The whole point is letting each new test case pull a small piece of new behavior out of you, one at a time.

A process is an independent unit of execution with its own memory space, file handles, and resources, isolated from other processes by the operating system. A thread is a unit of execution that lives inside a process and shares that process's memory space with every other thread in it.

That shared memory is exactly why threads are cheaper to create and communicate faster than processes, and exactly why they introduce race conditions that separate processes don't have to worry about. Two threads writing to the same variable without synchronization can corrupt it. Two processes can't touch each other's memory at all without explicit inter-process communication, which is slower but a lot harder to get subtly wrong.

TCP guarantees ordered, reliable delivery through acknowledgments and retransmission, at the cost of connection setup overhead and latency when packets need to be resent. UDP sends packets with no delivery guarantee, no ordering guarantee, and no connection setup, which makes it faster but leaves reliability entirely up to the application if it needs any at all.

UDP makes sense wherever a late packet is worse than a lost one: live video calls, multiplayer game state, DNS lookups. A video frame that arrives 400 milliseconds late is useless anyway, so TCP's retry logic just adds latency for no benefit. A banking transaction is the opposite case, where losing a packet silently is unacceptable and TCP's guarantees are exactly what you want.

Dependency injection means a class receives the objects it depends on from the outside, usually through its constructor, rather than constructing them itself internally. The class that needs a database connection doesn't instantiate one directly, it accepts one as a parameter.

java
// Without DI: hard to test, tightly coupled to a real database
class OrderService {
    private Database db = new PostgresDatabase();
}

// With DI: the caller decides what implementation to pass
class OrderService {
    private Database db;
    OrderService(Database db) { this.db = db; }
}

The concrete payoff is testability, which is exactly what ThoughtWorks interviewers are probing for here. With injection, a test can pass in a fake database that returns fixed data instantly, instead of standing up a real database connection just to test business logic that has nothing to do with SQL.

An abstract class can hold shared state and partial implementation that subclasses inherit directly, but a class can only extend one abstract class. An interface defines a contract with no shared implementation (outside default methods in newer language versions), but a class can implement as many interfaces as it needs.

Reach for an abstract class when several related types genuinely share behavior, not just a shape, and reach for an interface when you're defining a capability that unrelated types might all need to support. A Dog and a Car have nothing in common as classes, but both can reasonably implement a Comparable interface. ThoughtWorks pairing partners often ask this as a quick warm-up before moving into a design prompt.

The weak version of this answer is reciting the careers page back. The stronger version connects something specific ThoughtWorks is known for, its consulting model, its open-source contributions, its public stance on social and economic justice, to something concrete you've actually cared about in your own work, not just admired from a distance.

Interviewers here aren't looking for a rehearsed answer so much as a coherent one, something that would still make sense if they asked a follow-up about why that specific thing matters to you rather than letting you finish a memorized paragraph.

The strongest answers here describe a specific person and a specific gap, not "I always help junior developers." What did they not know, what did you actually do about it beyond answering questions when asked, and what changed for them afterward that you could point to concretely.

A genuinely useful detail to include: a time mentoring didn't go smoothly at first, and what you adjusted. That reads as more credible than a story where everything worked immediately, and it's closer to what real mentoring on a client engagement actually looks like.

This question is checking for honest self-assessment more than it's checking for a specific technical answer. A candidate who can't name a single decision they'd revisit reads as either inexperienced or unwilling to admit a mistake out loud, neither of which is a great signal for a consulting role where owning a wrong call in front of a client is part of the job.

The stronger answers explain what information was missing at the time the original decision got made, not just what you know now that makes the old choice look bad in hindsight. Hindsight makes everything look obvious; the useful part of the answer is what would have actually changed your mind back then.

ThoughtWorks moves consultants across client stacks regularly, so this question is really asking whether you have a repeatable way to get productive in something unfamiliar fast, rather than whether you happen to already know a long list of languages.

A specific, credible answer names the actual gap (a language, a framework, a domain you didn't know), what you did in the first week that mattered most, and where you were still weak a month in and how that showed up. Vague answers, "I'm a fast learner, I picked it up quickly", tend to get a direct follow-up asking for the actual timeline and what specifically you struggled with.

Medium questions

22

The cleanest version avoids ties breaking the query and doesn't assume a specific number of rows:

sql
SELECT MAX(salary) AS second_highest
FROM Employee
WHERE salary < (SELECT MAX(salary) FROM Employee);

This works because it sidesteps offset-based approaches entirely: find the maximum salary strictly less than the overall maximum, and that's the second-highest by definition, even if several employees share the top salary. A LIMIT/OFFSET version, ORDER BY salary DESC LIMIT 1 OFFSET 1, looks simpler but silently returns the wrong row when two people are tied for first place, since both rows have the same top salary and the offset just skips one of them.

In Python, -7 // 2 returns -4, not -3. Python's floor division always rounds toward negative infinity, so it takes the floor of -3.5, which is -4. Java and C truncate toward zero instead, so the equivalent expression in those languages gives -3.

The question is really testing whether a candidate has internalized that "integer division" isn't one universal rule across languages, it's a specific rounding behavior each language chose, and assuming your home language's behavior transfers is exactly the kind of bug that survives code review and shows up in production months later.

Parse each line, truncate the timestamp down to the hour, and use that truncated hour paired with a visitor identifier as a set key so repeat visits within the same hour don't get double-counted.

python
from collections import defaultdict

def unique_visitors_per_hour(log_lines):
    hourly = defaultdict(set)
    for line in log_lines:
        timestamp, visitor_id = parse_line(line)
        hour_bucket = timestamp.replace(minute=0, second=0, microsecond=0)
        hourly[hour_bucket].add(visitor_id)
    return {hour: len(visitors) for hour, visitors in hourly.items()}

This one is really a data-modeling question dressed up as a coding problem, and it's a favorite in ThoughtWorks technical screens for exactly that reason. The interviewer wants to see whether you reach for a set to dedupe correctly on your own, before being told the log has duplicate lines for a single visitor loading multiple assets in the same hour.

Floyd's cycle detection, two pointers moving at different speeds through the same list, is the expected answer. If there's no cycle, the fast pointer reaches the end. If there is one, the fast pointer eventually laps the slow pointer and they meet at the same node.

java
boolean hasCycle(Node head) {
    Node slow = head, fast = head;
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) return true;
    }
    return false;
}

O(1) space is the point of this solution over a hash-set approach that stores every visited node. The interviewer usually asks you to name that tradeoff even if you don't derive the algorithm from scratch under pressure.

Sort the intervals by start time first. Then walk through them once, and whenever the current interval's start is less than or equal to the end of the last merged interval, extend that merged interval instead of adding a new one.

python
def merge(intervals):
    intervals.sort(key=lambda x: x[0])
    merged = [intervals[0]]
    for start, end in intervals[1:]:
        last_end = merged[-1][1]
        if start <= last_end:
            merged[-1][1] = max(last_end, end)
        else:
            merged.append([start, end])
    return merged

Walk the list, and for each element check whether it's itself a list. If it is, recurse into it and extend the result; if it isn't, append it directly. Depth isn't known ahead of time, which is exactly why this has to be recursive rather than a fixed number of nested loops.

python
def flatten(lst):
    result = []
    for item in lst:
        if isinstance(item, list):
            result.extend(flatten(item))
        else:
            result.append(item)
    return result

Model heading as one of four discrete states, N, E, S, W, and represent turning as moving forward or backward through that cycle rather than as a pile of separate if-branches for every possible current-heading-plus-turn combination.

java
class Rover {
    int x, y;
    char[] headings = {'N', 'E', 'S', 'W'};
    int headingIndex;

    void turnRight() { headingIndex = (headingIndex + 1) % 4; }
    void turnLeft() { headingIndex = (headingIndex + 3) % 4; }

    void moveForward() {
        switch (headings[headingIndex]) {
            case 'N': y++; break;
            case 'E': x++; break;
            case 'S': y--; break;
            case 'W': x--; break;
        }
    }
}

The natural follow-up is adding obstacle detection, so a move command that would land on an obstacle gets rejected and reported instead of silently executed. Candidates whose original design cleanly separates "compute the next position" from "apply the move" tend to handle that follow-up in a couple of lines. Candidates who inlined everything into one big switch statement usually have to restructure first.

This is a bin-packing problem in disguise, and the expected approach is a greedy algorithm rather than an optimal one: sort talks by duration, then try to fill each session by picking talks that fit the remaining time, closest to full first.

python
def pack_session(talks, session_length):
    talks_sorted = sorted(talks, key=lambda t: -t.duration)
    session, remaining = [], session_length
    for talk in talks_sorted:
        if talk.duration <= remaining:
            session.append(talk)
            remaining -= talk.duration
    return session

Pairing partners usually ask you upfront whether you're solving for an optimal packing or a reasonable one, since true optimal bin packing is NP-hard. Saying that out loud and picking a greedy approach on purpose reads a lot better than silently building something that happens to be greedy without knowing why that's the right tradeoff.

This is a common ThoughtWorks pairing prompt because refactoring under someone's eyes reveals a lot more than a from-scratch problem does.

java
double calculateDiscount(Customer customer, Order order) {
    double discount = 0;
    if (customer.isPremium()) {
        if (order.getTotal() > 100) {
            discount = 0.2;
        } else {
            discount = 0.1;
        }
    } else {
        if (order.getTotal() > 200) {
            discount = 0.05;
        } else {
            discount = 0;
        }
    }
    return discount;
}

Guard clauses collapse this into a flat sequence of early returns, which reads top to bottom without the reader having to hold two levels of state in their head at once:

java
double calculateDiscount(Customer customer, Order order) {
    if (customer.isPremium() && order.getTotal() > 100) return 0.2;
    if (customer.isPremium()) return 0.1;
    if (order.getTotal() > 200) return 0.05;
    return 0;
}

The interviewer is watching for whether you refactor in small, verifiable steps, running the existing tests after each change, rather than rewriting the whole function at once and hoping it still behaves the same way.

Start with the simplest case: a light package to the nearest zone, and write one assertion for it before any implementation exists. Add a case for a heavier package. Add a case for a farther zone. Only after several concrete cases exist does a general formula, or a lookup table if the rules are irregular, get introduced.

python
def test_light_package_nearest_zone():
    assert shipping_cost(weight=1, zone=1) == 5.0

def test_heavy_package_nearest_zone():
    assert shipping_cost(weight=10, zone=1) == 15.0

def test_light_package_far_zone():
    assert shipping_cost(weight=1, zone=3) == 9.0

def shipping_cost(weight, zone):
    base = 5.0
    per_kg = 1.0
    per_zone = 2.0
    return base + per_kg * (weight - 1) + per_zone * (zone - 1)

The reason this shows up so often in the pairing round: it's a plausible five-minute slice of real client work, a pricing rule that will change again in six months, and it rewards writing the boring, obvious tests first instead of jumping straight to a clever general formula that might not match what the business actually needs.

Collisions are resolved either by chaining, where each bucket holds a small linked list (or, in modern Java, a balanced tree once a bucket gets large enough) of every entry that landed there, or by open addressing, where a colliding entry gets probed forward to the next available slot instead.

The consequence worth naming: a hash map's average-case O(1) lookup degrades toward O(n) if your hash function is bad enough that most keys collide into the same few buckets. A hash map is only as fast as its hash function is good at spreading keys evenly, which is why a poorly written equals/hashCode override on a custom object is a real, recurring source of production performance bugs.

Inheritance models an is-a relationship and locks it in at compile time, a Duck is-a Bird. Composition models a has-a relationship and can change at runtime, a Duck has-a FlyBehavior it can swap out.

java
// Inheritance: every Duck subclass is stuck with whatever fly() Duck defines
class Duck { void fly() { /* default flying */ } }
class RubberDuck extends Duck { void fly() { /* can't actually fly, awkward override */ } }

// Composition: behavior is a separate, swappable object
class Duck {
    FlyBehavior flyBehavior;
    void fly() { flyBehavior.fly(); }
}

The rubber duck case is the classic argument for composition: it inherits a fly() method that makes no sense for it and has to override it into a no-op, which is a sign the inheritance hierarchy modeled the wrong relationship in the first place. Composition sidesteps that by never forcing a shared method onto a type that doesn't actually need it.

Atomicity means the debit from one account and the credit to the other happen as a single unit, if the credit fails partway through, the debit rolls back too, so money never vanishes. Consistency means the transaction can't leave the database violating a defined rule, like a balance going negative when that's disallowed. Isolation means two transfers happening at the same time don't see each other's uncommitted intermediate state. Durability means once the transfer commits, it survives a crash immediately afterward.

Isolation is the one candidates usually understand least precisely, because different isolation levels trade correctness for throughput differently, and "isolation" doesn't mean total isolation by default in most databases unless you explicitly ask for the strictest level, which most systems don't use everywhere because of the performance cost.

A common one: a single class that both validates an order and sends a confirmation email, which violates single responsibility, because a change to email formatting now requires touching and retesting order validation code, and vice versa.

java
// Violates SRP: one class doing two unrelated jobs
class OrderProcessor {
    void validate(Order o) { /* ... */ }
    void sendConfirmationEmail(Order o) { /* ... */ }
}

// Split: each class has one reason to change
class OrderValidator { void validate(Order o) { /* ... */ } }
class OrderNotifier { void sendConfirmationEmail(Order o) { /* ... */ } }

A ThoughtWorks interviewer usually cares less about which SOLID letter you name and more about whether you can point to a real change that got harder because two unrelated responsibilities were tangled together in one class, since that's the actual cost the principle is trying to prevent.

Core entities: Book (the catalog entry), BookCopy (a physical instance of a Book, since a library owns several copies of the same title), Member, and Checkout (linking a specific BookCopy to a Member with a due date). The distinction between Book and BookCopy is the part candidates most often skip, and skipping it breaks the model the moment two people try to check out "the same book" and the system needs to know which physical copy is actually available.

java
class BookCopy {
    Book book;
    boolean checkedOut;
}

class Checkout {
    BookCopy copy;
    Member member;
    LocalDate dueDate;
}

A reasonable ThoughtWorks follow-up: how do you handle a hold queue when every copy of a popular title is checked out? That's a Reservation entity tied to a Book (not a specific copy), which gets promoted to a Checkout once any copy of that title becomes available.

This is the classic textbook example of the State pattern for a reason: a vending machine's behavior genuinely changes based on which state it's in. Selecting an item does nothing meaningful in an Idle state, but triggers dispensing in a HasMoney state. Modeling that as a chain of if-statements checking a status flag works at first and turns into an unreadable mess once a second state variable gets added.

java
interface VendingState {
    void insertCoin(VendingMachine m);
    void selectItem(VendingMachine m);
}

class IdleState implements VendingState {
    void insertCoin(VendingMachine m) { m.setState(new HasMoneyState()); }
    void selectItem(VendingMachine m) { /* no-op, no money inserted */ }
}

The interviewer wants the disagreement itself described honestly, not sanded down into "we talked it through and agreed." What was each side's actual position, what evidence or reasoning changed anyone's mind, and if nobody's mind changed, how did the team move forward anyway without it curdling into resentment.

A detail that reads well here: whether you were the one who was wrong. A story where you were right all along and eventually convinced everyone is fine, but a story where you changed your own position because of something a teammate pointed out shows the same skill from the harder side, and interviewers notice which one you default to telling.

This is close to the center of what a ThoughtWorks consultant actually does day to day, so a ThoughtWorks interviewer weighs the answer more heavily here than in most companies' behavioral rounds. What was the client attached to, what was actually wrong with it from your side, and how did you make the case without just pulling rank on expertise or steamrolling their stated priorities.

Strong answers usually involve showing rather than telling, a small prototype, a concrete cost estimate, a short pilot, something that let the client reach the conclusion largely on their own rather than being argued into agreeing on the spot.

A ThoughtWorks interviewer is checking two things at once here: whether you can hold a position under social pressure when you genuinely believe it's right, and whether you know when to stop pushing once you've made your case and the team has decided otherwise.

A red flag answer here is one where you were unpopular and turned out to be right, and the story ends with vindication and nothing else. The stronger version includes what you did in the meantime while the team disagreed with you, whether you kept working productively on the shared plan or quietly worked around it, since the second option is a much worse signal than losing the argument itself.

ThoughtWorks is public and specific about this being part of its identity, not a slogan bolted onto a careers page, so a generic answer about using tech for good tends to fall flat here. The stronger answers are concrete: a project where a technical decision had a real effect on who could access something, or a time you pushed back on a requirement because of who it would exclude, not an abstract statement of values with no example attached.

It's a fair answer to say you haven't had much direct exposure to this in your career so far, if that's honestly true, paired with a genuine account of why it matters to you anyway. What doesn't land well is an answer that sounds rehearsed specifically for this interview rather than something you'd say the same way in any other context.

A ThoughtWorks interviewer expects you to own the mistake specifically, what broke, how you found out, and what the immediate fix was, before getting to the more important part: what changed afterward so the same class of mistake doesn't happen again. A missing test, a missing alert, a process gap that let a bad deploy through unreviewed.

The weakest version of this answer blames the deploy pipeline, a teammate's review, or unclear requirements as the real cause and treats your own role as incidental. Even in situations where those things genuinely contributed, the strongest answers still find the part that was actually yours to own.

This is partly about ego management and partly about whether you can extract something useful from feedback that stings in the moment. A concrete example matters more than a general philosophy: a specific piece of feedback, what your first reaction actually was, not the reaction you wish you'd had, and what you changed afterward.

Candidates who can describe genuinely feeling defensive at first, and then explain what got them past that to actually engage with the feedback, tend to read as more self-aware than candidates who claim they always take criticism gracefully on the first try. Nobody does, every time, and pretending otherwise reads as either untrue or unreflective.

Hard questions

4

The expand-around-center approach handles both even and odd length palindromes cleanly in O(n squared) time and O(1) extra space, which beats the naive check-every-substring approach at O(n cubed) without needing the more involved O(n) Manacher's algorithm most interviewers won't expect from you.

python
def longest_palindrome(s):
    def expand(l, r):
        while l >= 0 and r < len(s) and s[l] == s[r]:
            l -= 1
            r += 1
        return s[l + 1:r]

    result = ""
    for i in range(len(s)):
        odd = expand(i, i)
        even = expand(i, i + 1)
        result = max(result, odd, even, key=len)
    return result

Every center, whether it's a single character (odd length) or the gap between two characters (even length), needs its own expansion call. Missing the even-length case is the single most common bug candidates ship here, and it fails silently on inputs where the correct answer spans two centers rather than one.

A hash map alone gives you O(1) lookup but no ordering, so you can't cheaply find the least-recently-used item to evict. A doubly linked list alone gives you ordering but O(n) lookup. Combine them: the hash map stores keys pointing directly to nodes in the linked list, and the list tracks recency by moving accessed nodes to the front.

python
class Node:
    def __init__(self, key, value):
        self.key, self.value = key, value
        self.prev = self.next = None

class LRUCache:
    def __init__(self, capacity):
        self.capacity = capacity
        self.map = {}
        self.head = Node(0, 0)
        self.tail = Node(0, 0)
        self.head.next = self.tail
        self.tail.prev = self.head

    def _remove(self, node):
        node.prev.next = node.next
        node.next.prev = node.prev

    def _add_front(self, node):
        node.next = self.head.next
        node.prev = self.head
        self.head.next.prev = node
        self.head.next = node

    def get(self, key):
        if key not in self.map:
            return -1
        node = self.map[key]
        self._remove(node)
        self._add_front(node)
        return node.value

    def put(self, key, value):
        if key in self.map:
            self._remove(self.map[key])
        node = Node(key, value)
        self.map[key] = node
        self._add_front(node)
        if len(self.map) > self.capacity:
            lru = self.tail.prev
            self._remove(lru)
            del self.map[lru.key]

ThoughtWorks interviewers watch for whether you reach for the combined structure directly or try a pure hash map first, then get stuck when asked how you'd find the least-recently-used entry without scanning everything.

A strike scores 10 plus the next two rolls. A spare scores 10 plus the next one roll. The trap is the tenth frame, which can have up to three rolls if the player strikes or spares there, and those bonus rolls have to be included without re-triggering another bonus lookup past the end of the game. ThoughtWorks pairing partners lean on this one specifically because it's easy to get almost right.

java
int score(int[] rolls) {
    int total = 0, rollIndex = 0;
    for (int frame = 0; frame < 10; frame++) {
        if (rolls[rollIndex] == 10) { // strike
            total += 10 + rolls[rollIndex + 1] + rolls[rollIndex + 2];
            rollIndex += 1;
        } else if (rolls[rollIndex] + rolls[rollIndex + 1] == 10) { // spare
            total += 10 + rolls[rollIndex + 2];
            rollIndex += 2;
        } else {
            total += rolls[rollIndex] + rolls[rollIndex + 1];
            rollIndex += 2;
        }
    }
    return total;
}

This is the kata I'd tell someone to actually practice more than any other on this page, not because it's algorithmically deep, it isn't, but because it punishes sloppy TDD specifically. Candidates who write one test for a gutter game, jump straight to a test with all strikes, and try to make both pass at once usually tangle themselves in an off-by-one bug in the bonus lookup that a smaller, more incremental set of tests would have caught two steps earlier.

ThoughtWorks pairing partners use exercises like this one, based on the well-known Gilded Rose refactoring kata, to see how you treat code you didn't write.

java
// Existing: updates item quality and sellIn each day
void updateQuality(Item item) {
    if (item.sellIn <= 0) {
        item.quality = Math.max(0, item.quality - 2);
    } else {
        item.quality = Math.max(0, item.quality - 1);
    }
    item.sellIn--;
}
// Task: add a "Conjured" item type that degrades in quality twice as fast as normal items

The move that works here is isolating the new behavior behind a check for the item's name or type, rather than threading new if-branches through the existing logic and risking a regression in how normal items already behave.

java
void updateQuality(Item item) {
    int degradeRate = item.name.equals("Conjured") ? 2 : 1;
    if (item.sellIn <= 0) {
        item.quality = Math.max(0, item.quality - (degradeRate * 2));
    } else {
        item.quality = Math.max(0, item.quality - degradeRate);
    }
    item.sellIn--;
}

Pairing partners are watching two things here specifically: whether you run the existing test suite before touching anything, so you know what "still works" means before you start, and whether your first instinct is to understand the existing logic's intent before editing it, rather than deleting and rewriting a function you don't fully trust yet.

Real-time scenario questions

5

Start with the entities: ParkingLot, Floor, ParkingSpot (with a size), and Vehicle (with a size and type). The interesting design decision, and the one a ThoughtWorks interviewer usually probes hardest, is how a vehicle finds an available spot, that logic belongs on the ParkingLot or a dedicated SpotAllocator, not scattered across Floor objects, since the allocation strategy is exactly the kind of thing that changes later, compact cars allowed in motorcycle spots, reserved spots for electric vehicles, and you want that change contained to one place.

java
class ParkingSpot {
    VehicleSize size;
    boolean occupied;
}

class ParkingLot {
    List<Floor> floors;
    ParkingSpot findSpot(Vehicle v) {
        for (Floor f : floors) {
            ParkingSpot spot = f.findAvailableSpot(v.getSize());
            if (spot != null) return spot;
        }
        return null;
    }
}

Two pieces matter more than the rest: how you generate a short, unique code for each long URL, and how you handle the read path, since a URL shortener is read-heavy by a wide margin, most short links get looked up far more often than they get created. Base62-encoding an auto-incrementing ID is the simplest reliable approach, it guarantees uniqueness without a collision check, unlike hashing the URL and hoping for no collisions.

python
ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

def encode(num):
    if num == 0:
        return ALPHABET[0]
    result = []
    while num:
        num, rem = divmod(num, 62)
        result.append(ALPHABET[rem])
    return "".join(reversed(result))

Caching the read path (the long URL for a given short code almost never changes once written) is the follow-up worth raising before being asked, since it's the single biggest optimization for a read-heavy system like this one.

Model Driver and Rider as separate entities with a location, and give Driver a status (available, en route, in trip). A MatchingService takes a ride request and filters drivers down to those marked available, then picks the closest one, the same shape as the parking-spot allocator above: filter first, rank second, and keep both steps as separate, swappable pieces of logic rather than one nested loop.

python
def find_driver(riders_location, drivers):
    available = [d for d in drivers if d.status == "available"]
    if not available:
        return None
    return min(available, key=lambda d: distance(d.location, riders_location))

The follow-up worth expecting: what happens if the closest driver declines the request? The matcher needs to fall back to the next-closest driver rather than failing outright, which is a strong signal for whether you designed ranking as a sorted list you can step through, instead of a single min() call with no fallback path.

The sliding-window-log approach stores a timestamp for every request per user, and on each new request drops timestamps older than one minute, then checks whether the remaining count is under N. It's exact but memory-heavy under high traffic. The token-bucket approach instead gives each user a bucket that refills at a fixed rate and drains one token per request, rejecting requests once the bucket is empty, trading a little precision for a fixed, small memory footprint per user.

python
class TokenBucket:
    def __init__(self, capacity, refill_rate):
        self.capacity = capacity
        self.tokens = capacity
        self.refill_rate = refill_rate
        self.last_refill = time.time()

    def allow_request(self):
        now = time.time()
        elapsed = now - self.last_refill
        self.tokens = min(self.capacity, self.tokens + elapsed * self.refill_rate)
        self.last_refill = now
        if self.tokens >= 1:
            self.tokens -= 1
            return True
        return False

Naming the memory tradeoff between the two approaches unprompted is usually what separates a strong ThoughtWorks answer here from an average one.

Core entities: Elevator (current floor, direction, a queue of requested floors), ElevatorController (decides which elevator answers a new request), and Request (a floor plus a direction, since a hallway button press specifies up or down, unlike a button pressed inside the car).

java
class Elevator {
    int currentFloor;
    Direction direction;
    TreeSet<Integer> upRequests;
    TreeSet<Integer> downRequests;
}

class ElevatorController {
    Elevator assignElevator(Request request, List<Elevator> elevators) {
        // pick the elevator with the least total work relative to request.floor
    }
}

The part that separates a strong answer here: recognizing that a hallway request (up or down at a specific floor) and a car request (a floor selected from inside a specific elevator) are genuinely different kinds of requests with different assignment logic, and modeling them as the same thing tends to produce a controller that can't correctly decide which idle elevator should answer a call.

How to prepare for a ThoughtWorks interview

Practice the ThoughtWorks kata round specifically, not generic LeetCode. Bowling Game, Mars Rover, and the String Calculator are all short enough to run through in under thirty minutes each, and the value isn't memorizing the solution, it's practicing writing the smallest possible failing test first and only generalizing once you have three or four real cases pulling you toward the general shape. Do each one twice, once alone and once talking out loud the whole time as if someone's watching, since narrating your reasoning under time pressure is its own skill separate from solving the problem correctly.

For the ThoughtWorks LLD round, sketch two or three of the designs above on paper before the interview, parking lot, library checkout, vending machine, and specifically practice explaining why you drew the class boundaries where you did. The actual answer matters less than being able to defend it when the interviewer changes one requirement partway through and asks what breaks in your design.

For the values and behavioral round, write down four or five real stories from your own work before the interview, not during it. A disagreement you had, a mistake you made, a time you pushed back on something. Vague behavioral answers built on the spot are the most common weak point candidates report after the fact, not the coding round.

Get the reps in before the real thing

Reading a kata's solution is not the same as writing tests for it live while someone asks why you chose that assertion first. LastRoundAI's mock interview mode runs live coding and pairing-style sessions with real-time follow-up questions in your browser, and the free plan includes 15 credits a month that reset monthly rather than piling up unused. Starter is $19/mo if a handful of sessions isn't enough runway before a real loop.

Once your reasoning holds up under a follow-up question, the slower part of the process is usually finding enough consulting and engineering roles that actually run a pairing-style loop instead of a pure algorithm screen. Auto-Apply queues tailored applications for your review, 10 a month on the free plan, up to 400 a month on the Ultimate plan, and nothing goes out until you approve it.

Questions about either product go to contact@lastroundai.com. That's the only inbox we check.

Leave a Reply

Your email address will not be published. Required fields are marked *