Asana makes work-management software, so the problems that fit its interviews are about tasks, projects, dependencies, notifications and many people editing the same data. Prepare the standard data-structure and algorithm set, then practice applying it to that kind of product: ordering tasks with dependencies, keeping a cache, handling real-time updates. This page gives you tested solutions for the likely coding problems and a plan for the design round.
Asana does not publish its interview script. Candidate reports describe the familiar shape: a recruiter call, a technical phone screen (45 to 60 minutes of coding), and a final loop with coding, system design and behavioral rounds. Some teams may add a take-home or a pairing session. Ask your recruiter what applies to your role, and which languages you can use.
What to know about the product and stack
Using the product for a week beats reading about it. Create a project, add dependencies, assign tasks, use a timeline view, and notice what updates live. Asana’s engineering page on its tech stack says it uses React on the web, native mobile apps, and an in-house reactive data layer called LunaDb, written in Scala, that pushes changes to subscribed clients; as of that page it was being modernized toward GraphQL-style queries. You will not be asked to reproduce this, but it shows what the company cares about: loading related data efficiently and keeping many clients in sync. Source: Asana’s tech stack page.
Coding problems, with solutions that run
Order tasks that depend on each other
This one is almost a literal feature of the product. Given tasks and “A must finish before B” dependencies, return a valid order, or report that no order exists because of a cycle. The standard solution is topological sort with in-degree counting (Kahn’s algorithm): start with tasks that have no unfinished prerequisites, and as you finish each, decrement its dependents.
from collections import deque, defaultdict
def schedule(tasks, deps):
"""deps: list of (before, after). Returns an order, or None if there is a cycle."""
out_edges, indeg = defaultdict(list), {t: 0 for t in tasks}
for a, b in deps:
out_edges[a].append(b)
indeg[b] += 1
q = deque(t for t in tasks if indeg[t] == 0)
order = []
while q:
t = q.popleft()
order.append(t)
for nxt in out_edges[t]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
q.append(nxt)
return order if len(order) == len(tasks) else None
print(schedule(["design", "build", "test", "ship"],
[("design", "build"), ("build", "test"),
("test", "ship"), ("design", "test")]))
# ['design', 'build', 'test', 'ship']
print(schedule(["a", "b"], [("a", "b"), ("b", "a")])) # None
O(V + E) time. If any task is left unprocessed, the graph has a cycle, which is exactly the case where the UI should refuse a new dependency. Follow-ups: detect the cycle when a single new edge is added (a DFS from the new edge’s target looking for its source), and find the longest chain of dependent tasks (the critical path) with dynamic programming over the topological order. Our graph DFS lesson covers the traversal underneath.
Detect a cycle in a linked list
Floyd’s algorithm: a slow pointer moves one step, a fast pointer two. If there is a cycle they meet; if not, the fast one reaches the end. O(n) time, O(1) space, which beats the hash-set solution on memory.
class Node:
def __init__(self, v):
self.v, self.next = v, None
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow, fast = slow.next, fast.next.next
if slow is fast:
return True
return False
a, b, c = Node(1), Node(2), Node(3)
a.next, b.next = b, c
print(has_cycle(a)) # False
c.next = b
print(has_cycle(a)) # True
LRU cache
A hash map plus recency order gives O(1) get and put. In Python OrderedDict does both jobs; in Java LinkedHashMap does. Be ready to build it from a hash map and a doubly linked list, and explain why each piece is there. The trade-offs against other policies are in our post on LRU and cache replacement.
Median from a data stream
Keep the smaller half in a max-heap and the larger half in a min-heap, with sizes differing by at most one. Adding a value is O(log n), and reading the median is O(1).
import heapq
class MedianFinder:
def __init__(self):
self.lo, self.hi = [], [] # lo is a max-heap (negated values)
def add(self, x):
heapq.heappush(self.lo, -x)
heapq.heappush(self.hi, -heapq.heappop(self.lo))
if len(self.hi) > len(self.lo):
heapq.heappush(self.lo, -heapq.heappop(self.hi))
def median(self):
if len(self.lo) > len(self.hi):
return -self.lo[0]
return (-self.lo[0] + self.hi[0]) / 2
m = MedianFinder()
for x in [5, 2, 8, 1]:
m.add(x)
print(x, m.median())
# prints: 5 5, then 2 3.5, then 8 5, then 1 3.5 (one pair per line)
Mention what changes if values come from a small range (a counting array) or if you only need an approximate median (a sketch).
Other problems worth practicing
- K-th smallest element in a binary search tree (in-order traversal, stop early).
- Serialize and deserialize a binary tree (preorder with null markers).
- Merge overlapping intervals, relevant for calendars and timelines.
- Find the missing number in 1..n: sum formula or XOR, O(n) time and O(1) space.
Those fundamentals are the base. If you are still shaky on them, Level Order Traversal and the Hash Map lesson are good places to start.
System design
For more senior roles, expect a design round. Prompts that fit the company: a task management system, a notification system, real-time collaboration on a shared document or list, search across a workspace, or tracking task dependencies. Work through “design a task manager” in this order:
- Requirements: create, assign and comment on tasks; projects and workspaces; permissions; notifications; search. State what you will not cover.
- Scale: number of users, tasks per project, read-to-write ratio. Projects are read far more than they are written.
- Data model: users, workspaces, projects, tasks, task-to-project membership (a task can live in several projects), dependencies, comments. Explain what you index and why.
- API: list tasks in a project with pagination, update a task, subscribe to changes.
- Real-time updates: clients hold a connection (WebSocket or similar) and receive change events for what they are looking at. Discuss what happens on reconnect, when events were missed (resend from a version number or reload), and conflicting edits (last write wins per field is the simple answer; say what you give up).
- Scale and failure: cache hot projects, shard by workspace so most queries stay on one shard, queue notifications so a slow email provider does not block writes.
The most useful habit is to name a trade-off at every step. Our trade-off analysis post explains how.
Behavioral questions
Typical ones: a challenging project and how you got past obstacles, a disagreement with a teammate, choosing between perfect code and a deadline, how you learn a new framework. Prepare real stories and tell them in the Situation, Task, Action, Result order. For a company that sells collaboration, how you work with others is a real part of the evaluation, so describe the role of the other people in your stories, not only your own.
A two-week plan
- Days 1 to 5: arrays, hash maps, linked lists, trees, graphs; one or two problems a day, solved aloud.
- Days 6 to 9: heaps, topological sort, intervals, and the four problems above written from memory in your interview language.
- Days 10 to 12: two design sessions with a friend, one of them on the task manager.
- Days 13 and 14: a mock interview, your behavioral stories, and questions for the interviewers. Sites like LeetCode and HackerRank have large problem banks, and AlgoCademy offers guided lessons if you want explanations as you go.
