← Asana Interview Insights

Asana·Software Engineer·Onsite - System Design / Architecture·Senior

SeniorPrefer not to say
Jul 2026

Summary

Asana software engineer interview, object-oriented design round. The whole session was built around designing 2048, which sounds like a toy problem until you're 20 minutes in and realize you've been thinking about it wrong.

Questions Asked (6)

Q1

Design the object model and core game logic for a 2048 game on an N x N grid. Focus on classes, responsibilities, relationships, and the algorithm for a move, not on UI or rendering.

System DesignAlgorithms & Data StructuresData Modeling
Author's notes

I started sketching a Board class and a Tile class, which is fine, but I immediately went down the road of writing four separate move handlers.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying requirements and constraints, then define the core classes (Board, Tile, Game) and their responsibilities. Walk through the move algorithm step-by-step, emphasizing the merge logic and direction handling, and discuss edge cases and extensibility.

Pro tip: Mention that you would separate the game logic from the UI to enable testing and reuse, and discuss how to handle moves in all four directions by transforming the board to a canonical orientation.

1. Clarify Requirements and Constraints

Ask about grid size (fixed or variable), win/lose conditions, scoring, and whether moves can be undone. Confirm that UI is out of scope.

2. Define Core Classes and Responsibilities

Identify Board, Tile, and Game classes. Board manages the grid and move logic; Tile represents a cell with a value; Game handles overall state, score, and spawning new tiles.

3. Design the Move Algorithm

Explain how to process a move in a given direction: compress non-empty tiles, merge adjacent equal values (each tile merges once per move), compress again, and update score. Use a canonical direction (e.g., left) and rotate/reflect the board for other directions.

4. Handle Game State and Edge Cases

After a move, check for win (2048 tile) or loss (no empty cells and no possible merges). Spawn a new tile (2 or 4) in a random empty cell. Discuss handling invalid moves (no change) and ensuring merges are not chained.

5. Discuss Extensibility and Testing

Mention how to extend for different grid sizes, add undo, or support different tile values. Emphasize unit testing the move logic with various board configurations.

Key Points to Mention

  • Separation of concerns: Board handles grid and move logic, Game manages state and score.
  • Move algorithm: compress, merge, compress, with merge-once-per-move rule.
  • Direction handling via board rotation/reflection to reuse a single move implementation.
  • Random tile spawning (90% 2, 10% 4) and checking for game over.
  • Score tracking: sum of merged tile values.
  • Immutability vs mutability: consider returning a new board state for easier undo and testing.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q2

How would you handle the 'each tile merges at most once per move' rule in your merge function, and what does a line like [2, 2, 2] produce?

Algorithms & Data StructuresTechnical Trade-offs
Author's notes

This is where I stumbled.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by explaining the merge rule clearly: each tile can participate in at most one merge per move, so merges must be processed left-to-right (or in the direction of movement) without reusing a tile. Then walk through the example [2, 2, 2] to show that the leftmost two 2s merge into 4, leaving the third 2 untouched, resulting in [4, 2]. Finally, discuss how you'd implement this in code, such as using a flag or processing in a single pass.

Pro tip: Mention that the rule prevents chain reactions (e.g., [2,2,4] should not become [8] in one move) and that you'd handle it by iterating in the direction of movement and skipping the next index after a merge. This shows you understand the game's mechanics and can translate them into clean code.

1. Clarify the rule

State that each tile can merge at most once per move, meaning after a merge, the resulting tile cannot merge again in the same move.

2. Process in movement direction

Explain that you iterate through the line in the direction of movement (e.g., left-to-right for a left move) and merge adjacent equal tiles, then skip the next index to avoid double merging.

3. Walk through the example

For [2, 2, 2], the first two 2s merge into 4, and the third 2 remains, giving [4, 2]. Emphasize that the new 4 does not merge with anything else in this move.

4. Discuss implementation

Describe how you'd code it: use a new array or in-place with a write index, and a flag to track whether the current tile has already merged.

5. Address edge cases

Mention cases like [2,2,4] -> [4,4] (not [8]) and [4,4,4] -> [8,4] to reinforce the rule.

Key Points to Mention

  • Each tile merges at most once per move, preventing chain merges.
  • Processing order matters: always merge from the direction of movement.
  • After a merge, skip the next tile to avoid reusing the merged tile.
  • Example [2,2,2] -> [4,2], not [8] or [2,4].
  • Implementation can use a write index and a 'merged' flag.
  • This rule is standard in 2048 and ensures predictable behavior.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q3

How would you inject randomness so the game logic is deterministically testable?

System DesignTechnical Trade-offs
Author's notes

Dependency injection.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by acknowledging the need to separate randomness generation from game logic, then propose dependency injection of a random number generator (RNG) interface. Explain how this enables deterministic tests by substituting a seeded or mock RNG, and discuss trade-offs like testability vs. production randomness.

Pro tip: Mention that you'd also make the seed configurable for reproducible bug reports in production, showing you think beyond just unit tests.

1. Identify randomness sources

List all places in the game logic where randomness is used (e.g., dice rolls, card shuffling, enemy AI).

2. Abstract randomness behind an interface

Define an interface (e.g., RandomProvider) with methods like nextInt(), shuffle(), etc., to decouple game logic from concrete RNG.

3. Inject the RNG dependency

Pass the RNG into game logic via constructor, method parameter, or a dependency injection framework, so it can be swapped.

4. Provide deterministic implementations for tests

Use a seeded RNG (e.g., java.util.Random with fixed seed) or a mock that returns preprogrammed sequences for predictable tests.

5. Discuss trade-offs and production use

Explain that production uses a secure or true random source, while tests use deterministic ones; also mention logging seeds for reproducibility.

Key Points to Mention

  • Dependency injection to invert control over randomness
  • Seeded random number generators for deterministic sequences
  • Mocking frameworks to stub random outputs
  • Separation of concerns: game logic vs. randomness generation
  • Reproducibility of bugs via seed logging
  • Trade-offs: test determinism vs. production unpredictability

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q4

How would you implement undo? Compare snapshotting the whole board versus recording a reversible move delta.

System DesignTechnical Trade-offsAlgorithms & Data Structures
Author's notes

Snapshotting is simpler but costs O(N^2) memory per undo step.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying the requirements: what operations need to be undone, how deep the undo stack should be, and whether memory or latency is more critical. Then compare snapshotting and delta-based approaches across dimensions like time/space complexity, implementation complexity, and edge cases, and recommend a hybrid or context-specific solution.

Pro tip: Mention that many production systems (e.g., Asana) use a hybrid: periodic snapshots plus deltas for recent changes, to bound memory while keeping undo fast. Also, discuss how to handle non-reversible operations (e.g., random moves) and concurrency.

1. Clarify requirements and constraints

Ask about board size, move frequency, undo depth, memory limits, and whether undo must be persistent across sessions. This determines the trade-off space.

2. Describe snapshotting approach

Explain that you store full board copies at each move. Highlight simplicity and fast undo, but note O(board size) memory per move and potential performance issues for large boards or deep undo stacks.

3. Describe delta-based approach

Explain that you record each move as a reversible operation (e.g., move piece from A to B, capture piece C). Highlight O(1) memory per move and scalability, but note complexity in implementing inverse operations and handling non-reversible moves.

4. Compare trade-offs

Contrast time/space complexity, implementation effort, and edge cases (e.g., random events, concurrent edits). Discuss how each approach handles undo depth and memory growth.

5. Propose a solution and justify

Recommend a hybrid or context-specific approach, such as snapshots every N moves plus deltas in between, or deltas with periodic compaction. Explain how it balances memory, performance, and complexity.

Key Points to Mention

  • Time and space complexity: snapshot O(board size) per move vs delta O(1) per move
  • Implementation complexity: snapshots are trivial; deltas require reversible operations and careful state management
  • Edge cases: non-reversible moves (e.g., random piece generation), concurrent edits, and undo/redo branching
  • Memory management: bounding undo history via snapshots every N moves or delta compaction
  • Performance: snapshot undo is O(1) restore, delta undo is O(k) where k is move complexity
  • Real-world examples: how systems like Asana or games implement undo with hybrid approaches

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q5

How would you detect game-over efficiently, and is 'board full' a sufficient condition?

Algorithms & Data StructuresSystem Design
Author's notes

No, board full is not sufficient.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by clarifying the game rules and board representation, then propose an incremental win-check that only examines lines through the last move, achieving O(1) per move. Finally, address whether 'board full' is sufficient by discussing draw conditions and edge cases like early wins.

Pro tip: Emphasize that checking only the last move's row, column, and diagonals is both efficient and correct, and mention that 'board full' alone is insufficient because a win can occur before the board is full.

1. Clarify the game and constraints

Ask about the game (e.g., Tic-Tac-Toe, Connect Four), board size, and win condition to tailor your solution. Confirm whether moves are always valid and if the board can be partially filled.

2. Define efficient detection strategy

Propose an incremental approach: after each move, check only the lines (row, column, diagonals) that include the last placed piece. This reduces time complexity to O(1) per move for fixed board sizes.

3. Analyze 'board full' condition

Explain that 'board full' is not sufficient for game-over because a player can win before the board is full. It only indicates a draw if no winner exists.

4. Handle edge cases and optimizations

Discuss handling multiple win lines, early termination, and potential optimizations like maintaining counts of consecutive pieces for larger boards (e.g., Connect Four).

5. Summarize and conclude

Reiterate that efficient detection checks only relevant lines after each move, and that 'board full' alone is insufficient; a win check must be performed independently.

Key Points to Mention

  • Incremental win-check: only examine lines through the last move.
  • Time complexity: O(1) per move for fixed board sizes, O(k) for k-in-a-row on large boards.
  • Space complexity: O(1) extra space if board is stored, or O(n) for board storage.
  • 'Board full' is a draw condition only if no winner exists; it is not sufficient for game-over.
  • Early termination: stop checking once a win is detected.
  • Edge cases: multiple simultaneous wins, invalid moves, and board size variations.

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.

Q6

How would you generalize the merge rule so that the slide logic doesn't need to change if the merge condition changes, for example Fibonacci-style merges or a different target tile?

System DesignTechnical Trade-offs
Author's notes

Extract the merge predicate as a strategy or function passed into the merge routine.

Create a free account to read the full note

AI HintsAI Generated

Suggested Approach

Start by identifying the varying parts of the merge logic and abstracting them behind a strategy interface. Then, show how the slide logic can depend on this abstraction, allowing different merge rules (e.g., Fibonacci, target tile) to be plugged in without modifying the slide code. Emphasize the use of dependency injection and the Open/Closed Principle.

Pro tip: Mention that you would use a factory or configuration to select the merge strategy at runtime, and highlight how this improves testability and extensibility. Also, note that you'd keep the strategy stateless to avoid side effects.

1. Identify the varying behavior

Analyze the current merge rule and determine what changes when the condition changes (e.g., merging criteria, target value). Separate the invariant slide logic from the variant merge logic.

2. Define a strategy interface

Create an interface (e.g., MergeStrategy) with a method like canMerge(tile1, tile2) and merge(tile1, tile2). This encapsulates the merge condition and result.

3. Implement concrete strategies

Provide implementations for different rules: standard 2048, Fibonacci, target tile, etc. Each strategy encapsulates its own logic.

4. Inject the strategy into slide logic

Modify the slide logic to accept a MergeStrategy instance (via constructor or method parameter). The slide logic calls the strategy without knowing the details.

5. Configure and test

Use a factory or configuration to select the appropriate strategy at runtime. Write unit tests for each strategy and for the slide logic with a mock strategy.

Key Points to Mention

  • Strategy pattern to encapsulate varying merge algorithms
  • Dependency injection to decouple slide logic from merge rule
  • Open/Closed Principle: open for extension, closed for modification
  • Factory or configuration for runtime strategy selection
  • Testability: mock strategies for unit testing slide logic
  • Stateless strategies to avoid side effects and ensure thread safety

AI-generated suggestions, not part of the candidate's original notes. May be inaccurate — verify before relying on them.