A candidate who solved all three problems on Atlassian's HackerRank online assessment still got dinged in the next round for something the test never measured: explaining why he picked a hash map over sorting for a problem that had an obvious follow-up about memory. Atlassian runs its early screen as a 90-minute, three-question DSA test on HackerRank, and candidate reports consistently note that solving all three matters more for advancing than solving them fast (InterviewDB, 2026). What trips people up isn't the algorithm, it's that Atlassian grades the technical rounds after the OA on code quality, tradeoff reasoning, and adaptability to changing requirements just as much as on a correct final answer, according to the company's own engineering interview guide (Atlassian Careers, Engineering Interview Guide).
The loop past the OA usually runs two coding rounds, a system design round, a manager conversation, and a separate values interview conducted by someone outside your hiring chain, built around Atlassian's five stated values: open company no bullshit, build with heart and balance, don't screw the customer, play as a team, and be the change you seek. System design questions lean hard on Atlassian's own product surface, candidates report being asked to design a multi-tenant issue tracker resembling Jira or a real-time collaborative document editor resembling Confluence, which makes sense for a company whose entire product line is collaboration software.
This page groups roughly 50 questions the way Atlassian's own loop is structured: the HackerRank online assessment, the coding/technical interview, system design, computer science fundamentals that show up in phone screens for some roles, the values interview, and the manager round. Answers are written the way a strong candidate would actually explain them out loud, not as a memorized script.
Online assessment: the HackerRank screen
Atlassian's first filter for most software engineering candidates is a timed HackerRank test, three problems in 90 minutes. Reports describe the difficulty as sitting between LeetCode easy and medium, with an emphasis on getting all three fully correct rather than partially solving more problems (InterviewDB, 2026). Binary search, greedy, and basic math problems appear often, with graph or dynamic programming problems showing up as the harder third question.
Easy questions
15This is a modified binary search run twice: once biased to find the leftmost occurrence, once biased to find the rightmost. A plain binary search stops the instant it finds a match, which throws away the boundary information you need, so both searches have to keep narrowing past a match instead of returning immediately.
def search_range(nums, target):
def find_bound(is_first):
lo, hi, res = 0, len(nums) - 1, -1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
res = mid
if is_first:
hi = mid - 1
else:
lo = mid + 1
elif nums[mid] < target:
lo = mid + 1
else:
hi = mid - 1
return res
return [find_bound(True), find_bound(False)]Interviewers watch for whether you catch the empty-result case (target not present at all) without a separate special-cased branch. The two-pass approach handles it for free, since both searches just return -1.
Sort by end time, not start time, and greedily keep an interval whenever its start is at or after the end of the last interval you kept. Sorting by start time is the mistake almost everyone makes first, because it feels more natural, but it leads to keeping a long interval early on that blocks out several shorter ones that would have fit better.
def max_non_overlapping(intervals):
intervals.sort(key=lambda x: x[1])
count, last_end = 0, float("-inf")
for start, end in intervals:
if start >= last_end:
count += 1
last_end = end
return countBoyer-Moore voting is the answer Atlassian's OA grader is almost certainly looking for, since a hash map solution passes correctness but not the O(1) space constraint some variants of this problem impose. Track a candidate and a count; increment the count when you see the candidate again, decrement otherwise, and swap the candidate whenever the count hits zero.
def majority_element(nums):
count, candidate = 0, None
for n in nums:
if count == 0:
candidate = n
count += 1 if n == candidate else -1
return candidateThe algorithm only guarantees a correct answer if a true majority element is known to exist. Without that guarantee, you need a second pass to verify the candidate's count actually exceeds n/2.
Sort each string's characters to get a canonical key, then bucket every original string into a hash map keyed on that sorted version. Two strings are anagrams exactly when their sorted forms are identical, so this reduces the whole problem to a single hash map pass after the sort.
def group_anagrams(strs):
groups = {}
for s in strs:
key = "".join(sorted(s))
groups.setdefault(key, []).append(s)
return list(groups.values())Reverse the entire character array first, then reverse each individual word within that reversed array back to normal order. Reversing twice, once globally and once per word, lands the words in the right order while each word's own letters stay correctly spelled.
def reverse_words(s):
chars = list(s.strip())
def reverse(lo, hi):
while lo < hi:
chars[lo], chars[hi] = chars[hi], chars[lo]
lo += 1
hi -= 1
reverse(0, len(chars) - 1)
start = 0
for i in range(len(chars) + 1):
if i == len(chars) or chars[i] == " ":
reverse(start, i - 1)
start = i + 1
return "".join(chars)A sliding window with a hash map of the last seen index for each character does this in one pass. When you hit a character already inside the current window, jump the window's left edge to just past that character's last position instead of sliding it forward one step at a time.
def longest_unique_substring(s):
last_seen = {}
start = max_len = 0
for i, ch in enumerate(s):
if ch in last_seen and last_seen[ch] >= start:
start = last_seen[ch] + 1
last_seen[ch] = i
max_len = max(max_len, i - start + 1)
return max_lenA process has its own isolated memory space, its own set of file descriptors, and its own copy of most OS-level resources. Threads within the same process share that memory space and most of those resources, each thread only keeps its own stack, registers, and program counter separate from the others.
Context switching between threads in the same process is cheaper because the memory mappings don't need to change, the CPU's translation lookaside buffer stays valid across the switch. Switching between processes usually means flushing or reloading that mapping, which is real overhead that adds up under heavy scheduling churn.
Physical (raw bits over a wire or radio signal), Data Link (Ethernet, MAC addressing), Network (IP, routing between networks), Transport (TCP and UDP, ports and reliable delivery), Session, Presentation, and Application (HTTP, DNS) sit on top. In practice, most day-to-day engineering conversations only need Network through Application, since the lower three layers are handled by hardware and drivers most developers never touch directly.
The layer distinction that comes up most often in real debugging is Transport versus Network: a ping succeeding but a connection timing out usually means the network layer is fine (packets are routable) but something at the transport layer, a firewall rule blocking a specific port, is the actual problem.
TCP guarantees ordered, reliable delivery through acknowledgments and retransmission, at the cost of connection setup overhead and higher latency under packet loss. UDP sends packets with no delivery guarantee and no ordering guarantee, but with far less overhead, since there's no handshake and no retransmission logic.
UDP is the right choice whenever a late packet is worse than a lost one, live video and voice calls, real-time multiplayer game state, and DNS lookups (where the client just retries after a short timeout rather than waiting on TCP's retransmission logic). Anything involving money, an inventory count, or a document edit needs TCP's guarantees, since a silently dropped update there is a correctness bug, not just a dropped video frame.
This maps to "play as a team," and the interviewer wants to hear about actual friction, different priorities, a team that didn't report to your manager and had no obligation to help you, not a story where everyone was already aligned from day one. Naming the specific coordination cost (a weekly sync that had to be created, a shared doc that didn't exist before) makes the story concrete instead of abstract.
What made the collaboration actually work is the part worth spending the most time on. Vague answers like "we communicated well" don't tell an interviewer anything; a specific mechanism, a shared Slack channel with a clear owner, a joint Confluence page both teams edited, does.
Pick a mistake that was actually yours, not a team failure you're describing in first person to avoid sounding like you're throwing anyone under the bus. Interviewers can usually tell the difference, and a story where the "mistake" is really someone else's decision reads as evasive.
The consequence should be real enough to matter, a production incident, a missed deadline that affected another team, and the recovery should describe concrete actions, not just "I learned from it." What you changed about how you work afterward is the part that actually answers the question.
This is "build with heart and balance," and the interviewer is listening for a real tradeoff, not a story where you secretly did it the right way and called it fast. Name the actual corner you cut, what risk that introduced, and whether you went back and fixed it later or decided it genuinely wasn't worth fixing.
A weak version of this answer treats "the right way" as always winning eventually, which isn't how real engineering tradeoffs usually resolve. Some shortcuts are permanent because the cost of revisiting them never justified itself, and saying that plainly is a more credible answer than pretending otherwise.
A weak answer here is generic enough to apply to any large software company, "great culture," "interesting problems," without anything tying it to Atlassian's actual products or the way the company describes itself. A stronger answer references something specific, having used Jira or Confluence in a previous job and having an opinion about it, or the remote-first, distributed-team structure Atlassian is known for running at scale.
It's fine, and often better, to name something you'd want to change or improve about a product you've actually used, rather than only praising it. That shows genuine familiarity rather than a rehearsed answer built from the careers page.
The answer that works best is a short narrative with a clear thread connecting each move, not a chronological list of job titles. Naming why you left a specific role, honestly, including if the reason was frustration or a bad fit, is more useful to the interviewer than a diplomatic non-answer, and it usually reads as more credible too.
Tie the "where next" half specifically to what this role at Atlassian would actually let you do that your current one doesn't. A vague answer here ("more growth," "bigger challenges") is the single most common way this question falls flat.
A strong answer is specific enough that the interviewer could actually check it a year later. Naming a concrete skill you'd want to have grown, a system you'd want to have owned end to end, or a kind of project you haven't gotten to lead yet, works better than a vague statement about "growth" or "impact."
It's worth tying the answer back to something you learned about the role or the team during the interview process itself, which signals you were actually listening during earlier rounds rather than reciting a prepared answer regardless of what you heard.
Medium questions
22A monotonic decreasing stack of (price, span) pairs solves this in a single pass. When today's price is greater than or equal to the price at the top of the stack, pop it and fold its span into today's running total, since every day that stack entry already covered is also covered by today.
def stock_span(prices):
stack = [] # (price, span)
result = []
for price in prices:
span = 1
while stack and stack[-1][0] <= price:
span += stack.pop()[1]
stack.append((price, span))
result.append(span)
return resultThe naive approach, walking backward day by day for each price, is O(n^2) in the worst case, a strictly decreasing sequence of prices followed by one huge spike. The stack keeps it O(n) because each element is pushed and popped at most once.
The water trapped above any bar is bounded by the shorter of the tallest wall to its left and the tallest wall to its right, minus the bar's own height. A two-pointer approach tracks the running max from both ends at once and only advances the pointer on the side with the smaller max, since that side's water level is already fully determined.
def trap(height):
left, right = 0, len(height) - 1
left_max, right_max = 0, 0
water = 0
while left < right:
if height[left] < height[right]:
left_max = max(left_max, height[left])
water += left_max - height[left]
left += 1
else:
right_max = max(right_max, height[right])
water += right_max - height[right]
right -= 1
return waterThe combination that gets you O(1) on both operations is a hash map for lookups paired with a doubly linked list for ordering. The hash map maps keys to nodes directly, so you never scan for a key, and the linked list lets you move a node to the front or evict the tail in constant time without shifting anything.
class Node:
def __init__(self, key, val):
self.key, self.val = key, val
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.cap = capacity
self.map = {}
self.head = Node(0, 0)
self.tail = Node(0, 0)
self.head.next, self.tail.prev = self.tail, self.head
def _remove(self, node):
node.prev.next, node.next.prev = node.next, node.prev
def _add_front(self, node):
node.next, node.prev = self.head.next, 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.val
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.cap:
lru = self.tail.prev
self._remove(lru)
del self.map[lru.key]Reports of this exact question surfacing at Atlassian go back years and it's a fair bar for a mid-level backend candidate, since the tricky part isn't any single operation, it's keeping the map and the list perfectly in sync across every method.
Sort by start time first, then walk through and merge whenever the current interval's start is at or before the end of the interval you're building. If it isn't, close out the interval you were building and start a new one.
def merge(intervals):
intervals.sort(key=lambda x: x[0])
merged = [intervals[0]]
for start, end in intervals[1:]:
if start <= merged[-1][1]:
merged[-1][1] = max(merged[-1][1], end)
else:
merged.append([start, end])
return mergedThis is a grouping-plus-heap problem dressed up in domain language, which is a pattern worth recognizing quickly in an interview instead of getting distracted by the file/collection framing. Sum file sizes into a hash map keyed by collection, then use a min-heap of size N to keep only the top N sums without sorting the entire collection list.
import heapq
from collections import defaultdict
def top_n_collections(files, n):
totals = defaultdict(int)
for name, collection, size in files:
totals[collection] += size
heap = []
for collection, total in totals.items():
heapq.heappush(heap, (total, collection))
if len(heap) > n:
heapq.heappop(heap)
return sorted(heap, reverse=True)The heap approach is O(k log n) where k is the number of distinct collections, versus O(k log k) to sort everything. For a small n relative to k, that difference is real and worth mentioning even if the interviewer doesn't push on it.
The snake's body is a deque, since you need fast additions at the head and removals at the tail on every move, plus a set holding the same coordinates for O(1) collision checks. Moving forward without eating pops the tail and pushes a new head; eating skips the pop, growing the snake by one segment.
from collections import deque
class SnakeGame:
def __init__(self, width, height, food):
self.width, self.height = width, height
self.food = deque(food)
self.snake = deque([(0, 0)])
self.body_set = {(0, 0)}
self.score = 0
def move(self, direction):
head_r, head_c = self.snake[0]
moves = {"U": (-1, 0), "D": (1, 0), "L": (0, -1), "R": (0, 1)}
dr, dc = moves[direction]
new_head = (head_r + dr, head_c + dc)
r, c = new_head
if not (0 <= r < self.height and 0 <= c < self.width):
return -1
tail = self.snake[-1]
ate = self.food and list(self.food[0]) == [r, c]
if not ate:
self.snake.pop()
self.body_set.discard(tail)
if new_head in self.body_set:
return -1
self.snake.appendleft(new_head)
self.body_set.add(new_head)
if ate:
self.food.popleft()
self.score += 1
return self.scoreThe subtle bug most candidates hit is checking self-collision before removing the old tail from the set. If the tail is about to vacate that cell this turn, the new head is allowed to move there, and checking against the stale set produces a false game-over.
Floyd's cycle detection, two pointers moving at different speeds through the same list. If a cycle exists, the fast pointer eventually laps the slow one and they land on the same node; if the list is acyclic, the fast pointer simply reaches the end first.
def has_cycle(head):
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return FalseThe follow-up worth knowing cold is finding where the cycle starts, not just whether one exists: once slow and fast meet, reset a third pointer to head and advance it and the meeting-point pointer one step at a time, they'll converge exactly at the cycle's entry node.
A standard flood fill, either DFS or BFS, from every unvisited land cell, marking every connected 1 as visited before moving to the next unvisited cell. Each fresh flood fill you start corresponds to exactly one new island.
def num_islands(grid):
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
visited = set()
def dfs(r, c):
stack = [(r, c)]
while stack:
row, col = stack.pop()
if (row, col) in visited or not (0 <= row < rows and 0 <= col < cols):
continue
if grid[row][col] == "0":
continue
visited.add((row, col))
stack.extend([(row+1, col), (row-1, col), (row, col+1), (row, col-1)])
count = 0
for r in range(rows):
for c in range(cols):
if grid[r][c] == "1" and (r, c) not in visited:
dfs(r, c)
count += 1
return countA pure entity-attribute-value table works but gets slow and awkward to query at scale, since every filter on a custom field turns into a join or a self-join. A hybrid schema is the answer that tends to satisfy both correctness and performance: keep the fixed, universal fields, title, status, assignee, as real relational columns with indexes, and store the tenant-defined custom fields in a JSONB column alongside them.
CREATE TABLE issues (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL,
assignee_id UUID,
custom_fields JSONB DEFAULT '{}'
);
CREATE INDEX idx_issues_custom_fields ON issues USING GIN (custom_fields);A GIN index on the JSONB column keeps ad hoc filtering on custom fields reasonably fast without forcing a migration every time a customer adds a field. The tradeoff worth naming out loud is that you lose strict type enforcement on those fields at the database level, so validation has to happen in the application layer instead.
Store each tenant's limit and current usage in a fast, shared store like Redis rather than in application memory, since a tenant's requests can land on any of several stateless API servers behind a load balancer and in-memory counters per server would undercount the true rate. A token bucket per tenant, refilled at a rate matching their plan tier, handles bursts more gracefully than a strict fixed window while still enforcing a hard ceiling.
Returning a clear 429 with a Retry-After header, and exposing the remaining quota in response headers on every call, is a small addition that saves a huge amount of support burden, since it lets client libraries back off correctly instead of hammering the API blind. The plan-tier limits themselves should live in a config table keyed by tenant, not hardcoded, since sales will change a specific customer's limit before engineering finds out about it.
A deadlock is a state where two or more processes are each waiting on a resource held by another process in the same cycle, so none of them can ever proceed. Four conditions all have to hold simultaneously: mutual exclusion (a resource can only be held by one process at a time), hold and wait (a process holds one resource while waiting for another), no preemption (a resource can't be forcibly taken away), and circular wait (a cycle of processes each waiting on the next).
Breaking any single one of those four conditions prevents deadlock entirely, which is why most practical fixes target just one, imposing a strict global ordering on lock acquisition breaks circular wait without touching the other three, and it's usually the cheapest fix to actually implement.
Atomicity means a transaction's operations all succeed or all roll back together, nothing partial. Consistency means a transaction moves the database from one valid state to another, respecting every constraint. Isolation means concurrent transactions don't see each other's uncommitted changes. Durability means once a transaction commits, it survives a crash.
A concrete isolation violation: two concurrent transactions both read an account balance of $100, both add a $50 deposit, and both write back $150, when the correct final balance after two $50 deposits should be $200. Without proper isolation (typically enforced through row-level locking or MVCC), the second transaction overwrites the first transaction's update instead of building on it, and $50 silently disappears.
Normalization organizes tables to reduce redundant data and avoid update anomalies, structured through a sequence of normal forms, each one addressing a specific kind of redundancy the previous form still allows. Third normal form, no non-key column depending on another non-key column, is the practical target most production schemas aim for.
The real cost of pushing normalization further than that is join overhead. A fully normalized schema for something like an order history page might require joining six or seven tables just to render one row, which is fine at small scale and genuinely painful once a table crosses tens of millions of rows. That's the entire reason denormalization, and read-optimized projections in general, exist as a deliberate practice rather than a mistake.
A race condition happens when two threads access shared state concurrently and the final result depends on the unpredictable order their operations happen to interleave in. A classic example is two threads both reading a counter's current value, both incrementing it in their own head, and both writing back, so one of the two increments is silently lost.
A mutex prevents this by making the read-modify-write sequence atomic from the perspective of every other thread: a thread has to acquire the mutex before touching the shared value, and every other thread trying to acquire the same mutex blocks until it's released. The cost is that a mutex you hold too long, or acquire in inconsistent order across different code paths, is exactly how deadlocks get introduced into otherwise correct code.
The honest version of this story has a moment where you actually said the disagreement out loud, in the room, rather than quietly complying and venting about it afterward. Interviewers are listening for whether you raised the concern directly and early, with a specific reason grounded in data or a tradeoff you'd already thought through, not just a gut feeling.
A strong answer names the actual disagreement, what you did to make your case (a prototype, a cost estimate, a small experiment), and what happened next, including if the stakeholder's original call turned out to be right and you were wrong. That last part matters more than people expect; a story where you were right every time reads as rehearsed rather than honest.
This one is aimed squarely at "don't screw the customer," and the answer that lands well is specific about the actual tension, a revenue target that would have been hit faster by shipping a feature with a known rough edge, a support cost that made a small customer segment expensive to keep happy, rather than a hypothetical.
What the interviewer is really checking for is whether you can name the tradeoff honestly instead of pretending the two sides never conflict. A good story acknowledges the business cost of the customer-first choice you made, and doesn't pretend it was free.
The detail that separates a real story from a generic one is what you actually said, word for word if you can remember it, not just "I told them their code needed work." Vague retellings of feedback conversations are the single most common way this answer falls flat.
Cover what prompted the feedback, how you delivered it (privately, specifically, tied to a concrete instance rather than a pattern you'd never mentioned before), and what changed afterward, or didn't. If nothing changed, say so and explain what you'd try differently next time; that's a more useful answer than a tidy success story.
This is the "be the change you seek" question almost word for word, and it's checking whether you actually took initiative or just complained about the process to your teammates. A strong answer names the specific broken thing (a deploy checklist nobody followed, a flaky test suite everyone just re-ran instead of fixing), what you did about it without being asked, and how you got buy-in from people who didn't report to you.
The weakest version of this answer describes a process improvement that was assigned to you by a manager. That's a fine story for a different question, but it doesn't answer this one, since the whole point is unprompted initiative.
This targets "open company, no bullshit" directly. The interviewer wants a specific example of bad news delivered clearly, a timeline slipping, a technical approach that had to be scrapped after real investment, rather than a story about softening bad news until it wasn't really bad news anymore.
What makes this land is naming exactly what you said and to whom, and what the immediate reaction was. A story where the bad news was received calmly because you'd been transparent all along, flagging risk early rather than only at the deadline, is a stronger answer than one where the honesty only showed up at the very end.
Naming a real clash, not a hypothetical, is what makes this answer useful to the interviewer. Maybe you prefer async written communication and a past manager wanted daily standups; maybe you like to think through a design alone before discussing it and a teammate wanted to whiteboard from the first minute.
What matters more than the clash itself is what you actually did about it, whether you adapted, asked for a compromise, or simply learned to work around it without either side changing. A story where the resolution was "we talked about it and found a middle ground" only works if you can say what that middle ground concretely was.
Pick something you can actually go deep on if pushed, since a shallow answer to a follow-up question here is a bigger red flag than picking a slightly less impressive system you know cold. Be ready to name the specific technology choices, why they were made at the time, and which of those choices you'd reverse with hindsight.
The "what would you change" half is the part that actually distinguishes a strong answer. Interviewers are listening for whether you can critique your own past decisions honestly, given the constraints (deadline, team size, unknowns at the time) you were actually working under, rather than either defending every choice or trashing all of them.
A real example matters more than a general philosophy here. Describe the actual disagreement, what evidence or reasoning you brought to the conversation, and how it actually resolved, including if you ended up deferring to your manager's call even after making your case.
Interviewers in this round are partly checking for fit with how Atlassian expects engineers to escalate disagreement, directly and with reasoning, not around someone's back and not by simply going along with a decision you privately think is wrong.
Hard questions
4This one comes up often enough in Atlassian reports that it's worth practicing by hand rather than only reading the solution. The trick is a doubly linked list of count-buckets, each bucket holding the set of keys that currently share that exact count, plus a hash map from key to which bucket it currently lives in.
Incrementing a key means finding its current bucket, moving the key into the next bucket up (creating one if it doesn't exist, right after the current bucket), and removing the current bucket if it's now empty. Decrementing is the mirror image, moving down instead of up and deleting the key entirely if its count drops to zero. Because the list of buckets stays sorted by count at all times, GetMaxKey and GetMinKey just peek at the tail and head bucket.
Most candidates can describe this design verbally faster than they can implement it correctly under time pressure, which is exactly why it separates strong performances from average ones on this question specifically.
CAP theorem states that during a network partition, a distributed system has to choose between consistency (every read gets the most recent write) and availability (every request gets a response, even if it might be stale). You can't have both during the partition itself, only ever two of the three properties, consistency, availability, partition tolerance, hold at once, and partition tolerance isn't really optional in a real distributed system.
For something like issue tracking, most teams lean toward availability during a partition, a slightly stale view of an issue is annoying, an outright error page blocking someone from working is worse, with eventual consistency reconciling the state once the partition heals. A payments or billing subsystem inside the same company would reasonably make the opposite tradeoff, since a stale balance is a much bigger problem than a temporary error.
This question is hardest precisely because most candidates want to reframe an actual failure as a near-success. Resist that. A real failure story names the goal that wasn't hit, the actual reason (not a euphemism for the reason), and what specifically the team or you would do differently with the same information available at the start.
Interviewers are less interested in assigning blame and more interested in whether you can look at a genuine loss clearly, without either over-blaming yourself or deflecting entirely onto factors outside anyone's control. A story with no lesson attached, or one where the lesson is suspiciously convenient, is the version that reads worst.
This question rewards specificity over diplomacy. Name the actual stack, the actual pain point, whether that's a monolith that's grown past the point a single team can safely deploy, a message queue chosen for reasons that no longer apply, or a testing setup that's slow enough that people skip running it locally.
The stronger half of the answer is what you'd actually propose instead, and an honest acknowledgment of the migration cost and risk involved in changing it, since a critique with no realistic path forward reads as complaining rather than engineering judgment.
Real-time scenario questions
8A sliding window log, one deque of timestamps per user, gives you exact correctness: on each request, pop expired timestamps off the front of the deque, then check if the remaining count is under the limit before appending the new timestamp. It's more memory than a fixed-window counter, but it doesn't have the fixed window's boundary bug where a burst right at the edge of two windows can let through nearly double the allowed rate.
from collections import deque
import time
class RateLimiter:
def __init__(self, max_requests, window_seconds):
self.max_requests = max_requests
self.window = window_seconds
self.requests = {}
def allow(self, user_id):
now = time.time()
q = self.requests.setdefault(user_id, deque())
while q and q[0] <= now - self.window:
q.popleft()
if len(q) < self.max_requests:
q.append(now)
return True
return FalseIf the interviewer pushes on memory at scale, the follow-up answer is a sliding window counter, two fixed buckets with a weighted average between them, which trades a small amount of accuracy for O(1) memory per user instead of O(N) timestamps.
Model each spot size (motorcycle, compact, large) as its own pool, since a motorcycle shouldn't get assigned a spot sized for a bus and a car shouldn't be blocked from a compact spot just because all large spots are full. Within each pool, a min-heap ordered by distance from the entrance gives O(log n) assignment and O(log n) release.
import heapq
class ParkingLot:
def __init__(self):
self.available = {"motorcycle": [], "compact": [], "large": []}
self.occupied = {}
def add_spot(self, size, spot_id, distance):
heapq.heappush(self.available[size], (distance, spot_id))
def park(self, vehicle_id, size):
if not self.available[size]:
return None
distance, spot_id = heapq.heappop(self.available[size])
self.occupied[vehicle_id] = (size, spot_id, distance)
return spot_id
def leave(self, vehicle_id):
size, spot_id, distance = self.occupied.pop(vehicle_id)
heapq.heappush(self.available[size], (distance, spot_id))Interviewers usually extend this into a discussion of what happens when a smaller vehicle takes a larger spot because its own pool is empty, a real tradeoff between utilization and correctness that has no single right answer, only a defensible one.
Base62 encoding of an auto-incrementing counter is the simplest approach that avoids collisions entirely, since every counter value maps to exactly one short code and there's nothing to check against. The tradeoff is that sequential codes are guessable and reveal roughly how many URLs the system has issued, which matters if that's sensitive.
An alternative is generating a random 7-character code and checking for a collision against the datastore before committing, which hides the sequence but adds a read-before-write on every creation and needs a retry loop for the rare collision. At real scale, the counter needs to be sharded, a range of IDs handed out per application server so no single counter becomes a write bottleneck, with each server encoding its own range into Base62 independently.
Reads dominate writes by a wide margin in this system, so most of the design effort should go into a cache in front of the datastore for the redirect lookup itself, not into optimizing the write path further than it needs to be.
A message queue sits between the event producers, comment added, mention, deadline approaching, and the channel-specific workers that actually deliver, so a slow email provider never blocks push notifications and a spike in one channel doesn't back up the others. Each channel gets its own consumer group reading off the same topic, which lets you scale email workers independently from push workers based on their very different throughput and latency characteristics.
User preferences (which events go to which channels, quiet hours, digest versus immediate) get checked once, early, before fan-out, not independently inside every channel worker, both to keep the logic in one place and to avoid a preference bug in one worker silently diverging from the others over time. Idempotency keys on every notification are non-negotiable, since retries after a transient failure are common and nobody wants three copies of the same push notification.
Model this as a layered override, not three separate systems: a default set of permissions at the organization level, project-level rules that can grant or restrict beyond that default, and issue-level rules that override both. Resolving a permission check means walking from most specific to least specific and stopping at the first explicit rule you find, falling back to the org default only if nothing more specific exists.
def resolve_permission(user, issue):
if rule := issue_level_rules.get((user, issue.id)):
return rule
if rule := project_level_rules.get((user, issue.project_id)):
return rule
return org_level_rules.get((user, issue.org_id), DEFAULT_DENY)The detail that matters at scale is caching this resolution per user per project, since recomputing a full permission chain on every single issue view in a large project would add real latency. Invalidating that cache correctly whenever a rule changes anywhere in the chain is the harder half of the problem, and it's the half interviewers actually want to hear you reason through.
Start by pinning down scale and the deployment model before drawing anything, because the right answer for a thousand small teams looks nothing like the right answer for a handful of massive enterprise tenants. For the common shared-database, shared-schema approach, every table carries a tenant_id column and every single query, without exception, filters on it, enforced at the query-builder or ORM layer so a missing filter fails loudly instead of silently leaking another tenant's rows.
Core entities are organizations, projects, issues, and users, with a many-to-many membership table linking users to projects inside their org. For large enterprise tenants that need stronger isolation guarantees or heavier usage, a hybrid model works: keep most tenants on the shared schema, but give the largest few their own schema or even their own database instance, migrated over once usage crosses a defined threshold.
The part interviewers actually probe on is what happens at the edges, a background job that iterates issues across all tenants for analytics, or a search index that has to respect tenant boundaries too. Row-level security at the database layer as a second line of defense, on top of application-level filtering, is the answer that tends to hold up under a skeptical follow-up.
The hard problem underneath the feature is conflict resolution when two people edit overlapping text at the same instant, and the two standard approaches are Operational Transform and CRDTs. OT transforms each incoming operation against every operation it missed so all clients converge on the same final document, which is what Google Docs uses and requires a central server to sequence operations. CRDTs give every character or block a unique, ordered identifier so operations commute regardless of arrival order, which fits a more distributed or offline-first architecture better but tends to carry more metadata overhead per edit.
Beyond the merge algorithm itself, the design needs presence (whose cursor is where, broadcast over a WebSocket connection per session), versioning (a durable append-only log of changes so you can reconstruct any prior revision), and permissions enforced at the same layer that applies edits, not just at the API gateway, since a permission check that only runs on page load doesn't stop a live edit from a user whose access was just revoked.
Fault tolerance matters more here than in most CRUD systems, because losing an in-flight edit is a much worse user experience than a slow page load. Buffering unacknowledged operations client-side and replaying them on reconnect is the detail that separates a design that sounds right from one that's actually been thought through.
An inverted index, Elasticsearch or OpenSearch in practice, sits alongside the relational database, since full-text search and relational query patterns want fundamentally different storage shapes. The database stays the source of truth for every write; the search index gets updated asynchronously, usually by consuming a change stream or an event emitted after each write commits, rather than by writing to both stores synchronously in the same request.
That asynchronous path means the search index is eventually consistent, a newly created issue might not be searchable for a few seconds, which is a tradeoff worth stating plainly rather than hiding, since it's the right tradeoff for search but would be the wrong one for the issue data itself. Tenant isolation applies here too, every search query needs a mandatory tenant filter baked into the query itself, not left to the caller to remember.
How to prepare for a Atlassian interview
Start with the HackerRank OA itself, not the harder rounds past it. Reports consistently note that solving all three problems fully matters more than solving one perfectly and rushing the rest, so practice finishing under real time pressure rather than untimed. Pick five or six binary search, greedy, and hash map problems and do them cold, on a clock, before moving on to harder graph or DP practice for the third question.
For the coding and system design rounds, practice explaining your reasoning out loud while you write, since Atlassian's own guide states explicitly that the company evaluates the tradeoff discussion as much as the final working code. For system design specifically, treat Jira and Confluence as the two products you should be ready to design a variant of; multi-tenancy, custom schemas, and real-time collaboration are the concrete themes that show up across candidate reports again and again, far more than generic system design prompts.
For the values round, write out eight or ten real stories from your own work history in advance, mapped loosely to Atlassian's five stated values, and practice telling each one in under two minutes. The stories that land best in mock sessions we've run at LastRoundAI are the ones with a specific, nameable moment, an exact sentence you said, a specific metric that moved, not a general description of "being collaborative" or "communicating well."
Get the reps in before the real thing
Reading through fifty questions is not the same as defending an answer out loud when an interviewer follows up with "what if the tenant count was ten times bigger" or "walk me through what you'd cut if you had half the time." LastRoundAI's mock interview mode runs live system design and behavioral rounds 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 $29/mo (or $19/mo on the 3-month plan) if a handful of sessions isn't enough runway before your loop.
Once your answers hold up under a follow-up, the slower part of the process is usually just getting in front of enough roles that actually run this kind of loop. 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.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
How many interview rounds does Atlassian have?
Usually a recruiter screen, a technical phone screen, then an onsite loop of four to five rounds covering coding, system design and behavioural. The exact count shifts by level and team, and some loops add a domain-specific round, so ask your recruiter for the actual schedule.
How hard is the Atlassian interview?
Hard, but the difficulty is more about depth of follow-up than exotic questions. Interviewers tend to take a reasonable problem and keep pushing on trade-offs, edge cases and what you would change at ten times the load. Preparing to be interrogated on an answer matters more than memorising more answers.
What coding level should I prepare for a Atlassian interview?
Comfortable medium-difficulty problems solved cleanly while talking, rather than hard problems solved silently. Most rejections come from unclear communication and untested edge cases, not from failing to find an exotic algorithm.
Does Atlassian ask system design at every level?
From mid-level upward, almost always, and it carries increasing weight as you go up. Junior loops may include a lighter version focused on structuring a feature rather than designing a distributed system.
