A candidate applying for a C++ Lead role in Bangalore described the whole loop as a single 1.5 hour session run over "EPAM online bridge," the company's internal video platform, with an architect who'd been writing systems software for two decades. No panel, no whiteboard theater, just move semantics, spinlocks, and a scrum conflict scenario back to back, and the interviewer stayed ten minutes past the slot to keep working through it. That mix, deep technical questions sitting right next to Agile role-play, is the pattern that shows up again and again in candidate reports about EPAM. It is a services and digital engineering company staffing client projects across banking, healthcare, and retail, and the loop is built to check whether you can write real code and also survive on a delivery team you didn't design.
For entry-level and internship hiring the process gets longer and more mechanical. One reported loop for a MERN stack internship ran an online test with two timed sections back to back, an aptitude and logical reasoning block candidates described as "very tricky," then a 120-minute technical MCQ and coding block covering Python, SQL, OS, and networking. That fed into a 30-minute group discussion on AI ethics, two technical interviews (one anchored on a CloudFront and AWS project, one working straight off the resume), and a closing HR round. Experienced-hire loops reported through Indeed skew shorter, typically three stages: recruiter screen, a technical test or interview, then a conversation with the hiring manager, with about a week to two weeks end to end and 83% of respondents saying the process fairly assessed their skills (Indeed company interview data).
This page pulls together 50 questions across the rounds candidates actually report: the online assessment, group discussion, coding and data structures, systems and concurrency, cloud and web architecture, databases, Agile and delivery practices, and behavioral or HR. Where a report only gave a topic and not the exact wording, we say so rather than inventing a specific phrasing that no one actually reported.
Online assessment: aptitude, logical reasoning, and technical MCQs
For campus and early-career hiring, the first gate is a proctored online test, not a human. Reports describe a short aptitude and logical reasoning block followed by a longer section mixing MCQs on core CS topics with a handful of coding problems.
Easy questions
19This block has nothing to do with your programming ability. It is measuring whether you can read a problem quickly, avoid the trap answer, and move on when a question is not worth the time. Candidates who described this section called it "tricky," which usually means the wrong answers are designed to be attractive to someone skimming.
Pace it by triage, not by order. Read every question once, answer anything you are sure of immediately, flag anything that needs a second read, and come back to flagged questions only after the easy ones are locked in. A wrong guess costs you the same as a blank, so never leave an answer empty if there is negative marking ambiguity, but never burn four minutes on one logic puzzle worth the same single point as the rest.
Treat the MCQs and the coding problems as two separate budgets inside the same clock. MCQs are worth doing fast because each one is low point value and low risk, so knock out every MCQ you know cold in the first pass, flag the ones you are unsure of, and do not let a single OS question about paging algorithms eat ten minutes you need for the coding section.
Coding problems are where the real signal lives, so protect at least half the total time for them even if that means leaving two or three MCQs unanswered. A blank MCQ costs you a few points. An untouched coding problem costs you the whole assessment.
TCP guarantees ordered, reliable delivery through acknowledgments, retransmission, and a three-way handshake before data flows, at the cost of overhead and latency. UDP sends packets with no handshake, no guaranteed order, and no automatic retransmission, trading reliability for speed and lower overhead.
The distinction interviewers actually probe for is when you'd pick one over the other in a real system: TCP for anything where losing or reordering data breaks correctness, an HTTP API call or a file transfer, and UDP for anything where a late or dropped packet is cheaper than the retransmission delay, live video, DNS lookups, or game state updates.
Restate the part of their point you actually agree with before you push on the part you don't. Something like acknowledging the underlying concern behind their position, then adding a specific counterexample or constraint they didn't account for. That keeps the exchange about the idea rather than about winning, and it's the same pattern that works in a real design review disagreement with a colleague.
Avoid flat contradiction ("that's wrong because...") as an opener. It reads as combative in a room where the interviewer is watching everyone's group dynamics, not just yours, and it tends to shut the other candidate down rather than build a better shared answer.
A recursive reversal works by peeling off one character, recursing on the rest of the string, then appending that first character after the recursive result comes back. Each call adds a frame to the call stack, and the reversal only happens because those frames unwind in the opposite order they were pushed, last in, first out.
def reverse(s):
if len(s) <= 1:
return s
return reverse(s[1:]) + s[0]
reverse("hello") # "olleh"Worth saying out loud in the interview: this is O(n) calls but each string slice and concatenation can be O(n) work depending on the language, so a naive recursive reversal on a long string is not free just because it's short to write. An interviewer who asks a follow-up about the stack is usually checking whether you actually understand why recursion works here, not just whether you can produce the syntax.
A stack is last in, first out: the most recently added item is the first one you get back, like a pile of plates. A queue is first in, first out: the item that's been waiting longest comes out first, like a line at a counter. The mental model that sticks is push versus enqueue happening at one end, and pop versus dequeue happening at either the same end (stack) or the opposite end (queue).
In practice, stacks show up wherever you need to undo something in reverse order, function call frames, undo history, matching parentheses, and queues show up wherever fairness or order of arrival matters, task scheduling, breadth-first traversal, a print job queue.
This is a direct application of a stack. Push every opening bracket you see, and every time you hit a closing bracket, check that it matches whatever's on top of the stack. If it doesn't match, or the stack is empty when you hit a closer, the string is unbalanced.
def is_balanced(s):
pairs = {')': '(', ']': '[', '}': '{'}
stack = []
for ch in s:
if ch in '([{':
stack.append(ch)
elif ch in pairs:
if not stack or stack.pop() != pairs[ch]:
return False
return not stack
is_balanced("{[()]}") # True
is_balanced("{[(])}") # FalseTrade space for time. Walk the array once, and for each number check whether the complement (target minus that number) already exists in a hash set you've been building as you go. If it does, you've found your pair. If not, add the current number to the set and keep going.
def two_sum(nums, target):
seen = set()
for n in nums:
if target - n in seen:
return (target - n, n)
seen.add(n)
return NoneThis runs in O(n) time with O(n) extra space, versus the naive nested-loop approach checking every pair, which is O(n^2) time but no extra space. Which tradeoff is better depends on whether memory or time is the tighter constraint, and saying that out loud is a stronger answer than just reciting the hash-set trick.
Walk both lists with a pointer each, and at every step attach whichever current node has the smaller value to the result, then advance that list's pointer. Once one list runs out, attach the remainder of the other list directly, since it's already sorted.
def merge(l1, l2):
dummy = tail = ListNode()
while l1 and l2:
if l1.val <= l2.val:
tail.next, l1 = l1, l1.next
else:
tail.next, l2 = l2, l2.next
tail = tail.next
tail.next = l1 or l2
return dummy.nextA mutex protects a critical section, a piece of code that touches shared state in a way that isn't safe if two threads execute it at the same time, a bank balance update, a shared counter increment, a linked list insertion. It guarantees mutual exclusion: only one thread can hold the lock at a time.
If two threads both try to lock the same mutex simultaneously, exactly one succeeds immediately and the other blocks, put to sleep by the OS scheduler until the first thread unlocks it. Neither thread proceeds into the critical section without holding the lock, that's the entire guarantee a mutex is providing.
A primary key uniquely identifies each row in a table, no two rows can share one, and it can't be null. A foreign key is a column (or set of columns) in one table that references the primary key of another table, and the database enforces referential integrity by rejecting any insert or update that would point at a primary key value that doesn't actually exist.
CREATE TABLE customers (id INT PRIMARY KEY, name TEXT);
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);That constraint is what actually prevents orphaned data, an order record pointing at a customer that was deleted or never existed. It's also why you'll hit an error trying to delete a customer row that still has orders referencing it, unless the foreign key is set up with a cascade or a nullify rule.
A unit test exercises one function or class in complete isolation, with any dependency it needs replaced by a mock or stub, checking that a single unit of logic behaves correctly regardless of what's around it. An integration test exercises multiple real pieces working together, an actual database, a real HTTP call between services, checking that the pieces cooperate correctly, not just that each piece is individually correct.
// unit test: mocks the repository entirely
var mockRepo = new Mock<IUserRepository>();
mockRepo.Setup(r => r.GetById(1)).Returns(new User { Id = 1 });
var service = new UserService(mockRepo.Object);
Assert.Equal("expected", service.GetUserName(1));
// integration test: hits a real (test) database
var response = await client.GetAsync("/api/users/1");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);Both matter, but they catch different classes of bugs. Unit tests catch logic errors fast and cheap. Integration tests catch the wiring errors, wrong connection string, mismatched serialization format between two services, that unit tests structurally cannot see because everything's mocked out.
Working software delivered frequently in small increments matters more than a complete upfront specification. Responding to changing requirements matters more than rigidly following an original plan. Direct collaboration with the people who actually use the software matters more than long written documentation as the primary communication channel.
The practical version of all three: you'd rather ship a smaller, working slice every couple of weeks and adjust based on real feedback than spend six months building the "complete" version of something nobody's validated yet. That's the mindset an interviewer is checking for, not whether you can recite the manifesto's four value statements verbatim.
The Product Owner owns the what and the why, prioritizing the backlog based on business value and making the call on what the team builds next. The Scrum Master owns the how of the process itself, removing blockers, facilitating ceremonies, protecting the team from scope creep mid-sprint, and coaching the team on Agile practice, but doesn't dictate what gets built.
A common confusion candidates have: the Scrum Master isn't a manager and doesn't assign tasks to individuals. Their authority is over the process, not over people or priorities, which is a distinction worth stating clearly if asked, since plenty of organizations blur this in practice even when the textbook definition is clean.
A retrospective is the team looking back at the last sprint to identify what worked, what didn't, and one or two concrete changes to try next sprint. It's not a status report and it's not a venting session, though it can start to feel like both if it isn't run well.
A useless retro produces a list of complaints with no owner and no follow-up, the same three issues get raised every two weeks and nothing changes. A useful one ends with a small number of specific, assigned action items that actually get checked at the start of the next retro, closing the loop instead of just generating more unaddressed feedback.
The weak version of this answer is generic, praising "great culture" or "growth opportunities" without anything specific to the company. A stronger answer names something concrete: the range of industries EPAM staffs projects into (banking, healthcare, retail, and more), the fact that you'd likely work across multiple client stacks rather than one product for years, or a specific technology area the company is known for that matches what you want more of on your own resume.
If you genuinely don't know much about the company beyond the job posting, say what specifically drew you to the role itself rather than faking company knowledge you don't have. An interviewer can tell the difference between researched specificity and a rehearsed line about "innovation" that would work for any company in the industry.
This is an invitation to control the narrative, not a request for your full resume read aloud. Pick two or three points that build a coherent line toward this specific role, not everything you've ever done. If you're moving from a smaller product company to a services firm, name that transition honestly, the appeal of working across multiple client domains rather than staying deep on one product, rather than dancing around it.
Keep it to two or three minutes. The interviewer already has your resume in front of them; what they're actually evaluating is whether you can communicate your own story clearly and whether the version you tell matches what's on paper.
Answer this directly and early rather than deflecting it, since availability and mobility genuinely affect which projects a services company can staff you on. If there are real constraints, say so plainly rather than agreeing to something you'll walk back later, since that costs you credibility with both the recruiter and whichever manager ends up staffing you.
If you're flexible, say what you're flexible about specifically, willing to travel occasionally but not relocate permanently, for example, rather than a vague "I'm open to anything," which recruiters tend to discount as not actually thought through.
Give a range grounded in research, not a single number pulled from nowhere. Look at what similar roles at comparable companies in your location and experience level are actually paying, and anchor your range there rather than either lowballing yourself out of anxiety or naming a number so high it signals you haven't looked into the market at all.
If you genuinely don't have enough information yet to give a confident range, it's fine to say so and ask what the budgeted range for the role is first. That's a normal, professional question, not an evasive one, as long as you follow it with your own number once you have that context.
Medium questions
22Process versus thread differences, scheduling algorithms (round robin, priority, first-come-first-served), paging and virtual memory, and deadlock conditions show up repeatedly across MCQ-heavy technical screens like this one. The trap is usually a question that sounds like a definition check but is actually testing whether you understand a consequence, for example asking which scheduling algorithm can cause starvation rather than just asking you to define round robin.
The fix is to study OS concepts as cause and effect rather than as a glossary. Know why priority scheduling without aging causes starvation, why a page fault handler has to be careful about re-entrancy, and why a deadlock needs all four Coffman conditions simultaneously, not just one of them. That framing survives a reworded question in a way that memorized definitions do not.
Read all three problem statements first, before writing a line of code, and rank them by how quickly you can picture a correct approach. Start with the one where you can already see the algorithm, not the one that looks most impressive to attempt. A fully working easy problem scores more than a half-written hard one on almost every automated grader.
If you're genuinely stuck mid-problem after several minutes, submit whatever partial solution passes at least the visible test cases before moving on, then come back if time remains. An automated grader gives partial credit for passing test cases; it gives zero credit for an elegant idea that never got submitted.
A group discussion is not a debate you're trying to win outright. It's a proxy for how you behave on a real project team: do you let one person dominate, do you build on someone else's point instead of just waiting for your turn to talk, do you stay engaged for the full 30 minutes or check out once you've said your piece.
The strongest move is usually to bring in a specific example rather than a general stance, something like a real case of biased training data or a specific regulatory angle, then explicitly connect it to a point someone else made. That signals both preparation and the ability to build collaboratively, which is exactly what a client-facing delivery team needs from a new hire.
You don't need domain expertise in ethics or policy to contribute something real. Bring the angle you actually have: as a backend developer you understand things like data provenance, audit trails, and how a system's logging decisions affect whether a biased outcome can even be traced back to its cause later. That's a genuinely useful contribution to an AI ethics conversation, and it's more credible than trying to fake philosophical depth you don't have.
What gets penalized is staying silent because the topic feels outside your lane, or overcorrecting into a generic opinion everyone in the room has already said. Find the specific, technical sliver of the topic you can speak to with real confidence, then speak to that.
The clean approach avoids computing lengths as a separate step. Walk two pointers, one starting at each list's head, and when a pointer reaches the end of its list, redirect it to the head of the other list. If the lists actually share a tail, both pointers will meet at the intersection node after at most one full pass through each list, because the total distance each pointer travels ends up equal.
def get_intersection(headA, headB):
a, b = headA, headB
while a is not b:
a = a.next if a else headB
b = b.next if b else headA
return a # None if no intersectionThe three scenarios worth naming explicitly, since interviewers reportedly walked through them one by one: equal-length lists that share a tail, list A longer than B, and list B longer than A. The pointer-swap trick handles all three without a branch, which is the actual point of the technique over a length-then-offset approach.
Floyd's cycle detection, two pointers moving at different speeds. A slow pointer advances one node at a time, a fast pointer advances two. If there's no cycle, the fast pointer reaches the end first. If there is a cycle, the fast pointer eventually laps the slow one and they land on the same node.
def has_cycle(head):
slow, fast = head, head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return FalseThe follow-up worth being ready for is finding where the cycle starts, not just whether one exists. Once slow and fast meet, resetting one pointer to the head and advancing both one step at a time will have them meet again exactly at the cycle's entry point, a result from the math behind Floyd's algorithm that's worth being able to state even if you don't derive it live.
This needs a LEFT JOIN from departments to employees, not an inner join. An inner join would silently drop any department that has no matching employee rows, which is exactly the case the question is asking you to preserve.
SELECT d.name, COUNT(e.id) AS employee_count
FROM departments d
LEFT JOIN employees e ON e.department_id = d.id
GROUP BY d.id, d.name;COUNT(e.id) rather than COUNT(*) matters here too. With a left join, a department with no employees produces one row with every employee column null, and COUNT(*) would still count that as one row, while COUNT(e.id) correctly counts zero since the id column itself is null.
Breadth-first traversal using a queue is the natural fit, since it visits nodes level by level rather than diving deep first. The trick to grouping by level is capturing the queue's size at the start of each iteration, before you start adding that level's children into it.
from collections import deque
def level_order(root):
if not root:
return []
result, queue = [], deque([root])
while queue:
level_size = len(queue)
level = []
for _ in range(level_size):
node = queue.popleft()
level.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
result.append(level)
return resultAn lvalue refers to an object that has a persistent, addressable location, something you could take the address of and that outlives the expression it appears in. An rvalue is a temporary, an expression's result that doesn't have a stable address you're expected to hold onto, a literal, the return value of a function, the result of an arithmetic expression.
C++11 cares because move semantics depend entirely on being able to tell them apart. If the compiler knows a value is an rvalue, a temporary about to be destroyed anyway, it can safely steal its internal resources (like a dynamically allocated buffer) instead of copying them, because nothing else will ever look at that temporary again.
A copy duplicates a resource, allocates new memory and copies every byte over. A move transfers ownership of the resource from the source object to the destination, leaving the source in a valid but unspecified, typically empty, state. The move constructor just steals the internal pointer and resets the source's pointer to null, no new allocation, no byte-for-byte copy.
class Buffer {
int* data;
public:
Buffer(Buffer&& other) noexcept : data(other.data) {
other.data = nullptr; // steal, don't copy
}
};
std::vector<int> a = {1, 2, 3};
std::vector<int> b = std::move(a); // a's internal buffer is now b's, a is emptyThe detail that separates a solid answer from a memorized one: std::move doesn't move anything itself, it's just a cast to an rvalue reference that makes the compiler eligible to call the move constructor overload instead of the copy constructor. The actual moving happens inside whatever move constructor gets selected.
Static binding resolves which function gets called at compile time, based on the declared type of the pointer or reference. Dynamic binding resolves it at runtime, based on the actual object's type, and in C++ that only happens for functions marked virtual, dispatched through a vtable lookup rather than a direct call.
class Animal {
public:
virtual void speak() { std::cout << "..."; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "Woof"; }
};
Animal* a = new Dog();
a->speak(); // "Woof", resolved at runtime via vtable, because speak is virtualWithout virtual, the same code would call Animal::speak regardless of the actual object, because the compiler resolves the call statically based on the pointer's declared type. That's the trap a lot of candidates fall into when asked this cold.
CloudFront is a content delivery network, a global network of edge locations that cache and serve content closer to the user than your origin server can. The first request for a given piece of content at an edge location is a cache miss and gets forwarded to the origin, but every subsequent request from a nearby user hits the cached copy at the edge, cutting latency and reducing load on the origin.
Beyond raw caching, it's also the layer where you'd typically terminate TLS close to the user, apply a web application firewall, and handle things like signed URLs for restricting access to private content. The design question worth being ready for is cache invalidation, since stale cached content after a deployment is the most common real production issue teams run into with a CDN in front of a frequently updated app.
.NET Framework is the original Windows-only runtime, still around for legacy apps but not receiving new feature investment..NET Core was the cross-platform rewrite, and starting with.NET 5, Microsoft dropped the "Core" branding and unified everything into a single yearly-versioned ".NET" release line that's cross-platform by default, runs on Windows, Linux, and macOS.
The practical takeaway for a candidate: if you're starting anything new today, it's plain ".NET" (currently on an actively supported LTS version), not.NET Framework, and knowing that.NET Framework 4.8 was effectively the last major release in that line is the kind of detail that shows you've kept up rather than learned.NET from an outdated tutorial.
ASP.NET Core requests flow through an ordered chain of middleware components, each one getting a chance to inspect or modify the request, then either passing it to the next component in the chain or short-circuiting the pipeline entirely, and then doing the same on the way back out with the response.
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => endpoints.MapControllers());Order matters here in a way candidates sometimes underestimate. Authentication has to run before authorization, because authorization needs to know who the user is before it can decide what they're allowed to do, and exception handling needs to wrap everything downstream of it, or an unhandled exception in a later middleware never gets caught.
A higher-order component (HOC) is a function that takes a component and returns a new component with extra behavior wrapped around it, a pattern for reusing logic like data fetching or auth checks across multiple components without copy-pasting that logic into each one.
// HOC pattern
function withAuth(Component) {
return function Wrapped(props) {
const user = useAuth();
if (!user) return <Redirect to="/login" />;
return <Component {...props} user={user} />;
};
}
// same behavior with a hook, no wrapper component needed
function Dashboard() {
const user = useAuth();
if (!user) return <Redirect to="/login" />;
return <div>Welcome, {user.name}</div>;
}Most of what HOCs were used for, sharing stateful logic across components, is handled more directly by custom hooks today, without the extra wrapper component in the tree and without the prop-naming collisions that stacking multiple HOCs used to cause. Knowing both patterns, and being able to say why hooks largely replaced HOCs for this use case, is a stronger answer than only knowing the newer one.
A pure reducer takes the current state and an action and returns a new state, with no side effects and no mutation of the existing state object. Redux's entire model, including time-travel debugging, undo, and React's ability to detect what changed and re-render efficiently, depends on being able to tell old state and new state apart by reference.
// broken: mutates state directly
function badReducer(state, action) {
state.count++; // mutation, React might not detect the change
return state;
}
// correct: returns a new object
function reducer(state, action) {
return {...state, count: state.count + 1 };
}If a reducer mutates state in place instead of returning a new object, the reference stays the same, and anything relying on reference equality to detect a change, React's shallow comparison in connected components, Redux DevTools' history, silently breaks or behaves inconsistently, often in ways that are hard to trace back to the actual mutation.
A normalized schema splits data into related tables to eliminate redundant storage of the same fact in multiple places, which prevents the update-anomaly problem where changing a customer's address means updating it correctly in every single order row that stores it, rather than one row in a customers table. It also enforces referential integrity through foreign keys in a way a flat table can't.
The tradeoff is query complexity and join cost. A wide, denormalized table is often faster to read for a specific reporting query because there's no join, at the cost of storage duplication and the update-anomaly risk. In practice, most production systems are normalized for their transactional writes and denormalize selectively, materialized views, read replicas shaped for reporting, for the read-heavy paths that actually need the speed.
The Scrum answer is that a sprint's scope is protected once it starts, so a genuinely new mid-sprint request goes to the Product Owner to prioritize into the backlog for the next sprint, not straight into the current one, unless the current sprint's goal is now actually broken without it. That protects the team's ability to commit to what they said they'd deliver.
In practice, at a services company staffing a client's team, the real answer is more subtle: you surface the tradeoff explicitly to the client rather than either just saying no or silently absorbing extra scope. Something like, "we can add this now, but it means something else already committed for this sprint slips, which would you rather have." That keeps the decision with the person who actually owns the priority call, instead of the engineering team quietly eating scope creep or unilaterally refusing a legitimate business need.
Story points are meant to be relative, not absolute, so the honest move with a truly unfamiliar story is to compare it against the closest thing the team has done before, even if the match is imperfect, and size it relative to that. If nothing comparable exists at all, flag the story as a spike candidate first, a small timeboxed research task to reduce the unknowns, rather than pointing a guess and hoping.
What you shouldn't do is silently inflate the estimate to cover your uncertainty without saying so. That just hides the risk instead of surfacing it, and it makes the team's velocity numbers meaningless over time. Naming the uncertainty out loud, "I genuinely don't know, here's my best guess and here's why it could be off," is a stronger answer than a confident number you don't actually believe.
This comes up often in services and consulting work specifically, since you don't get to pick your codebase the way you might at a product company. The honest answer acknowledges the friction rather than pretending it doesn't exist, and describes how you've handled it: picking your battles on what's worth pushing back on versus what's just a different-but-valid convention, and finding the moments where you can improve something without derailing the client relationship over it.
What interviewers are actually screening for here is adaptability without cynicism. Someone who complains about every client's codebase quality is a retention risk on a services team that moves between engagements regularly. Someone who can name a real frustration and also describe constructively working within it is a much safer bet.
Pick a real example with a specific technical disagreement, not a vague "we had different opinions" story. Explain what you actually did: did you raise it once and drop it, escalate it formally, propose a small experiment to settle it with data rather than opinion. The interesting part of this answer isn't whether you were right, it's how you handled being overruled if that's what happened.
A strong close acknowledges the outcome honestly, including if the senior person's call turned out to be the right one, or if it didn't and you note what you'd do differently next time you're in that position. Interviewers are wary of answers where the candidate is always right and everyone else was wrong; it reads as either dishonest or lacking self-awareness.
Name the actual change and the actual cost it created, not just "requirements changed and I adapted." Did you have to throw away working code, renegotiate a deadline, or push back on scope. The specificity is what makes this answer credible instead of generic.
The stronger half of the answer is what you did about the underlying cause, not just how you absorbed the immediate change. Did you push for a shorter feedback loop with the client so future changes get caught earlier, or a smaller first release so change requests come before more code is built on the wrong assumption. That shows you learned something structural from it, not just that you're personally flexible.
Hard questions
9Modified binary search. At each step, one half of the array (left of mid or right of mid) is guaranteed to be normally sorted, even though the whole array isn't. Figure out which half is sorted by comparing the endpoints, then check if the target falls inside that sorted half's range. If it does, search there; if not, search the other half.
def search(nums, target):
lo, hi = 0, len(nums) - 1
while lo <= hi:
mid = (lo + hi) // 2
if nums[mid] == target:
return mid
if nums[lo] <= nums[mid]:
if nums[lo] <= target < nums[mid]:
hi = mid - 1
else:
lo = mid + 1
else:
if nums[mid] < target <= nums[hi]:
lo = mid + 1
else:
hi = mid - 1
return -1The part candidates get wrong is trying to find the rotation pivot first as a separate step. It's unnecessary work; the comparison at each midpoint already tells you which half is sorted without a preliminary pass.
C++11 introduced unique_ptr and shared_ptr, but creating a unique_ptr required calling new directly and wrapping it, which meant the allocation and the ownership transfer were two separate steps with a window for an exception between them to leak memory. C++14 added std::make_unique, closing that gap by allocating and wrapping in a single expression, mirroring make_shared which C++11 already had.
// C++11 style, has an exception-safety gap
std::unique_ptr<Widget> w(new Widget());
// C++14, single expression, no gap
auto w = std::make_unique<Widget>();The practical rule that follows: prefer make_unique and make_shared over calling new directly whenever you're constructing a smart pointer, for exactly this exception-safety reason, not just for the shorter syntax.
A semaphore is a counter with atomic increment and decrement, and a thread blocks on decrement if the count is zero. It's good for controlling access to a fixed pool of N interchangeable resources, N database connections, N worker slots, because the count directly models how many are available.
A condition variable is for waiting on an arbitrary, application-defined condition becoming true, and it always pairs with a mutex protecting that condition, because you need to check the condition and start waiting atomically to avoid a race between checking and sleeping. It's the right tool when the wait condition isn't a simple count, "the queue has an item," "the buffer isn't full," "initialization finished," things you'd otherwise have to hack together with a semaphore and extra bookkeeping.
A spinlock makes a waiting thread busy-loop, repeatedly checking if the lock is free, instead of yielding to the scheduler and sleeping. That wastes CPU cycles while waiting, which sounds strictly worse than sleeping, but it avoids the cost of a context switch, which on modern hardware can be more expensive than a short spin if the lock is expected to be held for only a few instructions.
The rule of thumb: spinlocks make sense for very short critical sections on a multi-core system where the lock is expected to be released quickly, so the busy-wait is shorter than a context switch would cost. They're a bad choice for anything that might block for a while, I/O, a long computation, because then you're burning a whole core for nothing while another thread does real work.
The classic deadlock scenario is two threads each holding one lock and waiting on the other, thread A holds mutex 1 and wants mutex 2, thread B holds mutex 2 and wants mutex 1, neither can proceed. The most common real fix is establishing a global lock ordering: every thread that needs multiple locks always acquires them in the same defined order, so the circular-wait condition that deadlock requires can never form.
// both threads always lock in the same order: mutexA before mutexB
std::lock(mutexA, mutexB); // C++11's std::lock avoids the ordering problem entirelyC++'s std::lock (or std::scoped_lock since C++17) is worth mentioning here specifically, since it locks multiple mutexes together using a deadlock-avoidance algorithm internally, which is a cleaner answer than just saying "always lock in the same order and hope everyone follows the convention."
They can look similar in a diagram (both gate access to something), but they solve different problems. A spinlock is purely about mutual exclusion for a single resource with a very short expected wait, and it wastes CPU while waiting. A semaphore tracks a count of available resources and puts waiting threads to sleep rather than spinning, and it can allow more than one thread through at once if the count is greater than one, which a spinlock protecting a single resource never does.
If someone asks this and you only have one thing to say, say the count. A binary semaphore (count of 1) resembles a mutex conceptually, but a spinlock is never counting anything past 0 or 1 and a general semaphore can gate access for N concurrent holders.
The C# compiler compiles source into Intermediate Language (IL) and metadata, packaged into an assembly, not directly into machine code. When you run the application, the Common Language Runtime loads that assembly and the Just-In-Time compiler translates IL into native machine code for the actual CPU it's running on, method by method, as each method is first called.
The CLR is also doing a lot more than JIT compilation while the app runs: garbage collection, type safety verification, exception handling, and thread management all happen underneath your code without you writing that infrastructure yourself. That's the actual value proposition of a managed runtime versus a language that compiles straight to native code, you trade some raw performance and startup time for memory safety and a garbage collector you don't have to write.
Cached objects at the edge don't know your origin changed unless something tells them to. If your cache behavior sets a long TTL (time to live) on an asset without a cache-busting strategy, CloudFront keeps serving the old cached copy at every edge location until that TTL expires naturally, regardless of what's live on the origin now.
The real fix is versioned asset names, a content hash in the filename for anything static, so a new deployment produces genuinely new URLs and there's nothing stale to invalidate. For anything you can't version this way, an explicit CloudFront invalidation request purges the cached copy across edge locations immediately, but invalidations aren't free at high volume and they're a reactive patch, not a substitute for a cache strategy that doesn't need them in the first place.
Count frequencies with a hash map first, that part's O(n) and non-negotiable. The interesting decision is how you get the top k out of that frequency map. A full sort of all distinct elements by frequency is O(m log m) where m is the number of distinct elements, which is fine for most interview-sized inputs but not the tightest answer.
import heapq
from collections import Counter
def top_k_frequent(nums, k):
counts = Counter(nums)
return [item for item, _ in heapq.nlargest(k, counts.items(), key=lambda x: x[1])]A min-heap of size k gets you O(m log k) instead, which matters when k is small relative to the number of distinct elements. Bucket sort by frequency gets you O(m) if you want to push further, since frequency is bounded by the array's length. Knowing all three options and which one to reach for given the actual constraints is the real signal here, more than being able to code any single one from memory.
How to prepare for an EPAM Systems interview
Prepare for the specific stage you're actually facing rather than generic interview prep. If you're early career, the online assessment is the real gate, so practice timed MCQ blocks across OS, networking, and SQL alongside two or three coding problems under a clock, not just untimed practice on a coding site. If you're mid-to-senior, expect the interview to sit close to your resume, deep questions on whatever cloud service, framework, or language you listed, not a generic system design prompt disconnected from what you've actually built.
Across the loop, whatever your level, budget real preparation time for the Agile and behavioral questions, not just the technical ones. A services company staffing client teams is explicitly screening for how you behave under scope conflict, client pressure, and disagreement with teammates, and candidates who spend all their prep time on coding and none on articulating a real project story tend to stumble on questions that have nothing to do with syntax.
LastRoundAI's mock interview mode runs live rounds with real-time follow-up questions, useful for rehearsing the resume-based technical interviews and the behavioral scenarios candidates reported, not just abstract coding drills. The free plan includes 15 credits a month that reset monthly, and Starter is $19/mo if you need more runway before the real thing.
Once your answers hold up under a follow-up question, Auto-Apply queues tailored applications for your review across services and consulting firms, 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.
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
Is the EPAM aptitude test hard to clear?
It is more about pace than difficulty. The quantitative and logical sections are standard campus-placement material, but the time per question is tight, so accuracy under time pressure is what actually filters people out. Practising with a clock matters more than learning new topics.
What should freshers focus on for a EPAM interview?
One programming language you can write confidently on paper, core CS fundamentals (OOP, DBMS, operating systems, networks), and a project you can explain end to end. Freshers who can defend their own project in detail consistently do better than those who list five projects shallowly.
Does EPAM ask coding questions in the interview?
Usually yes, though at a moderate level rather than competitive-programming difficulty. Expect array and string manipulation, basic recursion and SQL. Being able to explain your approach out loud carries real weight, because the interviewer is assessing trainability as much as correctness.
What happens in the EPAM HR round?
Relocation flexibility, shift willingness, the service agreement or bond if one applies, and why you want this company specifically. It is largely a fit and commitment check, but people do get rejected here, usually for appearing uninterested or for contradicting something on their form.
How long does the EPAM hiring process take?
Commonly two to six weeks from first assessment to offer, though campus drives can compress it to a few days and lateral roles sometimes stretch longer. Silence in between is normal and is not usually a signal either way.
