One candidate on an on-campus Virtusa drive for a Java software engineer role watched the shortlist shrink three times in one day: 496 people sat the online test, 66 cleared it, 26 made it through the technical round, and 11 walked out with an offer letter. That funnel, and the fact that the online test was computer-adaptive with sectional timers you couldn't go back and revisit once a section closed, is the kind of detail that doesn't show up in a generic "top interview questions" list, but it tells you exactly what to expect (GeeksforGeeks, Virtusa interview experience).
Virtusa is a 32,000-plus employee IT services and digital engineering company running delivery out of 71 locations, and most of its hiring is built around staffing Java, data, and QA roles onto client projects rather than building a single in-house product. That shapes the interview itself. Expect an online assessment heavy on core computer science MCQs (operating systems, networks, software engineering, testing) alongside two or three coding problems, then a technical round that spends most of its time on Java fundamentals, collections, and SQL rather than open-ended system design, then a short, low-pressure HR round.
Multiple candidate write-ups describe the same shape with small variations: sometimes it's three rounds, sometimes four, and the technical round runs anywhere from 20 to 60 minutes depending on the role and how much project discussion happens. What's consistent is that the bar is "can you explain core Java and SQL cleanly," not "can you whiteboard a distributed cache." The questions below reflect that.
Online assessment: coding and MCQs
The first round candidates describe is a timed online test, sometimes three hours long, split into sections you can't revisit once the clock on that section runs out. It mixes two or three coding problems in Java or C/C++ with roughly 60 MCQs spanning operating systems, DBMS, networks, software engineering, and testing.
Easy questions
22It stops candidates from parking on the coding section and running out the clock there while skipping the MCQ section entirely, or the reverse. Locking each section behind its own timer forces you to actually attempt every part of the test within a fixed budget, which is closer to how the company wants to compare candidates across a large applicant pool fairly.
The practical effect for you as a candidate: budget your time per section before you start, not per question. If a section is 45 minutes and has 20 MCQs and 2 coding problems, decide up front roughly how many minutes each coding problem gets, because once that section closes you cannot go back and finish a half-written function.
A process is an independent unit of execution with its own memory space, file handles, and address space. A thread is a unit of execution inside a process that shares that process's memory and resources with other threads in the same process. Creating a new process is expensive because the OS has to allocate a full separate memory space; creating a thread is cheap because it reuses the parent process's memory.
It shows up in these tests because it's a fast way to check whether a candidate actually understands concurrency at the OS level rather than only at the language level. Someone who can explain why threads share a heap but each get their own stack has a real mental model, not just a memorized definition.
TCP is connection-oriented: it establishes a handshake, guarantees ordered delivery, and retransmits lost packets, at the cost of extra overhead and latency. UDP is connectionless: it fires packets with no handshake, no ordering guarantee, and no retransmission, which makes it faster but unreliable.
You pick TCP whenever correctness matters more than speed, an HTTP request, a file transfer, a database connection. You pick UDP when speed and low latency matter more than guaranteed delivery of every packet, live video, VoIP, DNS lookups, where losing an occasional packet is cheaper than the delay of retransmitting it.
Black-box testing checks a system's behavior against its specification without looking at the internal code, you feed it inputs and verify the outputs match expectations. White-box testing looks at the internal logic and structure of the code itself, checking that specific branches, loops, and paths execute correctly.
A QA engineer writing test cases from a requirements document is doing black-box testing. A developer writing unit tests that specifically target an if-branch they just added is doing white-box testing. Most real test suites use both, black-box for functional coverage and white-box for code-path coverage.
Binary search repeatedly halves a sorted array's search space by comparing the target against the middle element and discarding the half that can't contain it. The classic bug is computing mid as (low + high) / 2, which can overflow for very large arrays in languages with fixed-width integers, though in Java specifically this only matters once low + high exceeds Integer.MAX_VALUE.
int binarySearch(int[] arr, int target) {
int low = 0, high = arr.length - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
if (arr[mid] == target) return mid;
if (arr[mid] < target) low = mid + 1;
else high = mid - 1;
}
return -1;
}low + (high - low) / 2 avoids the overflow entirely because the intermediate sum never exceeds high. It's a small habit worth having automatically rather than something you reason through under interview pressure.
The iterative version walks the list once, keeping a running pointer to the previous node so each node can flip its next pointer to point backward instead of forward. The recursive version does the same thing from the tail end inward, but costs O(n) stack space that the iterative version doesn't.
ListNode reverseIterative(ListNode head) {
ListNode prev = null;
while (head != null) {
ListNode next = head.next;
head.next = prev;
prev = head;
head = next;
}
return prev;
}
ListNode reverseRecursive(ListNode head) {
if (head == null || head.next == null) return head;
ListNode newHead = reverseRecursive(head.next);
head.next.next = head;
head.next = null;
return newHead;
}Two strings are anagrams if they contain exactly the same characters with the same frequencies, just rearranged. The cleanest approach is a single frequency-count array (26 slots for lowercase English letters), incrementing for characters in the first string and decrementing for the second, then checking that every slot ends at zero.
boolean isAnagram(String s, String t) {
if (s.length() != t.length()) return false;
int[] counts = new int[26];
for (char c : s.toCharArray()) counts[c - 'a']++;
for (char c : t.toCharArray()) counts[c - 'a']--;
for (int count : counts) if (count != 0) return false;
return true;
}The brute-force version checks every pair, O(n squared). The better answer is a single pass with a HashMap: for each number, check whether target minus that number has already been seen, and if not, store the current number and move on.
int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> seen = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (seen.containsKey(complement)) {
return new int[]{seen.get(complement), i};
}
seen.put(nums[i], i);
}
return new int[]{-1, -1};
}An abstract class can hold constructors, instance fields, and a mix of implemented and unimplemented methods, but a class can only extend one abstract class. An interface historically held only method signatures and constants, though since Java 8 it can also have default and static methods with bodies, and a class can implement as many interfaces as it wants.
Pick an abstract class when subclasses share real, reusable implementation and state, a base Animal class with a shared age field and a move() method that subclasses override. Pick an interface when you're describing a capability that unrelated classes might all implement, Comparable, Runnable, Serializable, none of which imply a shared parent.
Polymorphism means the same method call resolves to different behavior depending on the actual runtime type of the object, even though the reference variable's declared type stays the same. Java gets this through method overriding for runtime polymorphism and method overloading for compile-time polymorphism.
class Shape {
double area() { return 0; }
}
class Circle extends Shape {
double radius;
Circle(double r) { radius = r; }
@Override
double area() { return Math.PI * radius * radius; }
}
Shape s = new Circle(5);
s.area(); // calls Circle's area(), even though s is declared as ShapeA static variable belongs to the class itself, not to any individual instance, so every object of that class shares the exact same copy of it. A static method belongs to the class as well, which is why it can be called without creating an instance, and it can only directly access other static members, not instance fields, because it has no this reference to work with.
class Counter {
static int totalCount = 0;
int id;
Counter() {
totalCount++;
id = totalCount;
}
}
// every Counter instance increments the same shared totalCountEncapsulation bundles data with the methods that operate on it and restricts direct access to that data from outside the class. Making fields private is the mechanism, but the actual point is control, deciding exactly what other code can read, write, and validate about an object's internal state through a defined set of methods rather than reaching in and mutating fields directly.
class BankAccount {
private double balance;
void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Deposit must be positive");
balance += amount;
}
double getBalance() { return balance; }
}Without encapsulation, any code holding a reference to a BankAccount could set balance to a negative number directly. The private field plus a validating deposit() method is what actually enforces the invariant, the visibility keyword alone does nothing without the validation logic behind it.
ArrayList backs its storage with a resizable array, so random access by index is O(1), but inserting or removing from the middle requires shifting every element after that position. LinkedList is a doubly linked list, so inserting or removing at a known position is O(1) once you're there, but getting to an arbitrary index requires walking the list from one end, O(n).
Pick ArrayList when you mostly read by index and rarely insert in the middle, which describes the overwhelming majority of real code. Pick LinkedList only when you're doing frequent insertions or deletions at known positions, like implementing a queue or a deque, and even then ArrayDeque usually beats LinkedList in practice because of better cache locality.
A List preserves insertion order and allows duplicate elements, accessed by index. A Set enforces uniqueness, no two elements can be equal according to equals() and hashCode(), and most Set implementations don't guarantee any particular iteration order (though LinkedHashSet and TreeSet are exceptions that do).
List<Integer> list = new ArrayList<>(List.of(1, 2, 2, 3));
Set<Integer> set = new HashSet<>(list);
System.out.println(list.size()); // 4
System.out.println(set.size()); // 3, duplicate removedThe uniqueness constraint exists because a HashSet is really a HashMap beneath that where every element is a key mapped to a dummy value, and a HashMap can't have two entries with the same key, that's the whole mechanism a Set is built on top of.
The commonly cited phases are requirements gathering, design, implementation, testing, deployment, and maintenance. Testing isn't a single phase bolted on at the end, unit tests get written during implementation, integration and system testing happen once components come together, and acceptance testing happens right before deployment, with maintenance often surfacing bugs that trigger a new, smaller cycle through some of those same phases.
The waterfall model treats these phases as strictly sequential, one finishes completely before the next starts. Agile methodologies break the same phases into small, repeated cycles (sprints), so you're doing requirements, design, implementation, and testing repeatedly on small slices of the product rather than once on the whole thing.
Waterfall commits to a full set of requirements up front and moves through design, build, and test as one long sequential pass, with change becoming expensive the further along the project gets. Agile assumes requirements will change and builds in short iterations, delivering working increments frequently and adjusting based on feedback each cycle.
Waterfall still makes sense on projects with genuinely fixed, well-understood requirements and regulatory sign-off gates, certain government contracts, safety-critical embedded systems, where changing scope mid-project is either impossible or extremely costly. Most client-facing software work benefits from Agile's feedback loop, which is why it's the default at most IT services companies today.
A primary key uniquely identifies each row in its own table and can't be null. A foreign key is a column (or set of columns) in one table that references a primary key in another table, enforcing that any value stored there actually exists in the referenced table.
A table can have only one primary key, though that key can be composite, made up of multiple columns together. A table can have as many foreign keys as it has relationships to other tables, an orders table might have a foreign key to customers and a separate foreign key to a warehouses table, for example.
The failure mode here is describing the project in the abstract, "we built a web app that did X," without ever answering what you personally did. Interviewers at Virtusa specifically probe project discussions to distinguish a candidate who understood a system's design decisions from one who copied code from a teammate or a tutorial.
The stronger version names a specific technical decision you made or a specific bug you tracked down, and explains the reasoning, not just the outcome. If you used Dijkstra's for a routing feature, be ready to explain why you picked it over a simpler BFS, what the graph actually represented, and where the algorithm's assumptions (non-negative edge weights) mattered to your real data.
This question shows up in the HR round specifically, according to multiple candidate reports, and it is not really about your exact five-year plan. It is about whether your stated goals are plausible given the role you're interviewing for. An answer that's wildly misaligned with what the job actually offers, wanting to run your own startup in eighteen months while interviewing for a long-term services role, raises a flag about retention, not ambition.
A grounded answer names a real skill or responsibility you want to grow into that the role plausibly leads toward, deeper ownership of a specific technology area, moving from individual contributor toward a technical lead role, and connects it honestly to why this particular company and role is a reasonable step toward that, not a scripted answer that could apply to any company.
This is a logistics question, not a values test, and the honest answer matters more than a rehearsed one, because a mismatch here (an unrealistic notice period, an unwillingness to relocate for a role that requires it) surfaces eventually regardless of what you say in the interview. If you have a genuine constraint, state it plainly and ask whether there's flexibility rather than agreeing to something you can't actually deliver.
If relocation is genuinely fine but conditional on something, timeline, family logistics, say so specifically rather than giving a flat yes that creates a problem two weeks before your start date.
A generic answer, "you're a big respected IT company," signals you didn't look past the job posting. A better answer references something specific: the kind of client engagements Virtusa runs (digital engineering and consulting work across banking, healthcare, and insurance clients, among others), the scale of the delivery organization, or a specific practice area relevant to the role you applied for.
Tie it back to your own goals honestly. If you're early career, it's fair to say you want structured exposure to enterprise-scale systems and client-facing delivery work that a smaller company wouldn't offer yet. That's a real reason, not filler.
This shows up in candidate reports as a genuinely casual question meant to assess communication style and comfort in conversation, not a trick question with a correct answer. Treat it as exactly what it looks like, a chance to talk naturally for a minute, rather than searching for a hidden "right" answer.
The only real misstep is over-preparing a stiff, corporate-sounding answer here. HR rounds like this one are partly checking whether you can hold an unscripted conversation, which matters on a job that involves client-facing communication.
Medium questions
22A deadlock is a state where two or more processes are each waiting on a resource held by another, so none of them can ever proceed. The four necessary conditions are 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).
Break any one of those four conditions and a deadlock becomes impossible in that system. In practice, most real systems prevent deadlocks by attacking hold-and-wait or circular-wait, for example by always acquiring locks in a fixed global order so a cycle can never form.
This is the kind of straightforward aggregation query that shows up in the coding section alongside the MCQs, and it tests whether you know GROUP BY and HAVING actually belong together rather than reaching for a WHERE clause on an aggregate, which doesn't work.
SELECT department_id, COUNT(*) AS employee_count
FROM employees
GROUP BY department_id
HAVING COUNT(*) > 5;WHERE filters rows before grouping happens, so you can't reference COUNT(*) in it, the aggregate doesn't exist yet at that stage. HAVING filters groups after aggregation, which is exactly what you need here.
The naive version checks only that a node's left child is smaller and its right child is bigger, but that's wrong, a node three levels down on the left subtree still has to be smaller than the root, not just smaller than its immediate parent. The fix is to carry a valid range down through the recursion and tighten it at every step.
boolean isValidBST(TreeNode node, long min, long max) {
if (node == null) return true;
if (node.val <= min || node.val >= max) return false;
return isValidBST(node.left, min, node.val)
&& isValidBST(node.right, node.val, max);
}Using long for the bounds instead of int avoids an edge case where a node's value equals Integer.MIN_VALUE or MAX_VALUE and would otherwise break the strict inequality check.
This is Kadane's algorithm and the trick is realizing you only need to track two numbers as you scan once: the best sum ending exactly at the current index, and the best sum seen anywhere so far. At each index you decide whether extending the previous subarray is better than starting a fresh one at the current element.
int maxSubArray(int[] nums) {
int currentSum = nums[0], maxSum = nums[0];
for (int i = 1; i < nums.length; i++) {
currentSum = Math.max(nums[i], currentSum + nums[i]);
maxSum = Math.max(maxSum, currentSum);
}
return maxSum;
}Candidates who reach for a brute-force nested loop first (checking every subarray, O(n squared)) can usually still pass if they get there, but naming Kadane's by name and explaining why it's O(n) is what separates a clean answer from a slow one.
Floyd's cycle detection, the "tortoise and hare" approach, uses two pointers moving through the list at different speeds, one node at a time and two nodes at a time. If there's a cycle, the faster pointer eventually laps the slower one and they meet inside the loop. If there's no cycle, the faster pointer simply reaches the end first.
boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}A sliding window with a set or map tracking characters currently inside the window solves this in one pass. Expand the window's right edge by one character at a time; if that character is already in the window, shrink the window from the left until the duplicate is gone.
int lengthOfLongestSubstring(String s) {
Set<Character> window = new HashSet<>();
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
while (window.contains(s.charAt(right))) {
window.remove(s.charAt(left));
left++;
}
window.add(s.charAt(right));
maxLen = Math.max(maxLen, right - left + 1);
}
return maxLen;
}final on a variable means it can only be assigned once, after which any reassignment is a compile error. final on a method means subclasses can't override it, the implementation is locked in at that level. final on a class means nothing can extend it at all, String and all the boxed number types in Java are final for exactly this reason.
The common thread across all three is "this is fixed, don't try to change it further down the hierarchy." Interviewers usually follow up asking why String is final, and the real answer touches immutability and security, since Java relies on Strings being unchangeable for things like class loading and network connection parameters that get passed around as Strings.
The servlet container loads the servlet class, calls init() exactly once to set up any one-time resources, then calls service() for every incoming request, which internally dispatches to doGet(), doPost(), or another method depending on the HTTP verb. destroy() runs once when the container is shutting the servlet down, giving it a chance to release resources cleanly.
public class MyServlet extends HttpServlet {
@Override
public void init() { /* runs once, on first load */ }
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) {
/* runs on every GET request */
}
@Override
public void destroy() { /* runs once, on container shutdown */ }
}The detail that trips people up: init() and destroy() each run exactly once per servlet instance, while service()/doGet()/doPost() run once per request, and the container typically keeps a single servlet instance alive across many concurrent requests rather than creating a new one each time.
Overloading means multiple methods in the same class share a name but differ in parameter type, count, or order, resolved at compile time based on the arguments you pass. Overriding means a subclass redefines a method it inherited with the exact same signature, resolved at runtime based on the actual object's type.
class Calculator {
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; } // overload
}Yes, you can overload a static method, since overloading is purely a compile-time, name-and-signature concern. You can't override a static method the way you override an instance method though, if a subclass declares a static method with the same signature, it's method hiding, not overriding, and which version runs is decided by the declared type of the reference, not the actual object.
The JVM tracks which objects are still reachable from a set of root references, active thread stacks, static fields, and so on, and periodically reclaims memory for objects that are no longer reachable from any root. Most JVMs use a generational approach: new objects go into a young generation that's collected frequently and cheaply, and objects that survive several collections get promoted to an older generation that's collected less often but with more thorough algorithms.
You can't force garbage collection to happen on a specific schedule, calling System.gc() is only a suggestion to the JVM, not a command, and relying on it running at a particular moment is a bug waiting to happen. The interview follow-up usually asks about memory leaks in a garbage-collected language, and the honest answer is that they still happen, most commonly through objects that stay reachable longer than intended, a static collection that keeps growing, or listeners that never get unregistered.
== compares references, whether two variables point to the exact same object in memory..equals() compares content, whether two objects represent the same logical value, and String overrides equals() specifically to do a character-by-character comparison rather than the default reference check inherited from Object.
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // false, different objects
System.out.println(a.equals(b)); // true, same content
String c = "hello";
String d = "hello";
System.out.println(c == d); // true, both point to the same interned literalThat last line is where people get tripped up. String literals are interned by the JVM into a shared pool, so two literals with identical text really do end up as the same object, but new String() explicitly forces a new object, and relying on == to compare Strings is a bug that only shows up once someone stops using literals and starts building strings dynamically.
Checked exceptions extend Exception (but not RuntimeException) and the compiler forces every caller to either catch them or declare them in a throws clause. Unchecked exceptions extend RuntimeException and impose no such requirement, they can surface anywhere without any caller being forced to acknowledge them at compile time.
void readFile(String path) throws IOException { // checked, caller must handle
Files.readString(Path.of(path));
}
void divide(int a, int b) { // unchecked, no throws clause required
int result = a / b; // ArithmeticException if b is 0
}The design tension is real. Checked exceptions force callers to think about failure modes, but overusing them clutters method signatures with exceptions the caller usually can't do anything meaningful about besides log and rethrow. Most modern Java codebases lean toward unchecked exceptions for anything that represents a programming bug and reserve checked exceptions for genuinely recoverable conditions the caller is expected to handle differently.
A HashMap stores entries in an array of buckets, and a key's hashCode() determines which bucket it lands in after being run through the map's own hash-spreading function. Two keys with different hashCode() values can still land in the same bucket if their hashes collide after that spreading step, and Java handles this by chaining, each bucket holds a linked list (or, since Java 8, a balanced tree once a bucket gets large enough) of all entries that landed there.
Map<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
// if "apple" and "banana" hash to the same bucket, both live in that
// bucket's linked list (or tree), and get() walks the list comparing
// keys with.equals() until it finds a matchThe follow-up question worth being ready for: why does HashMap require both a correct hashCode() and a correct equals() on custom key objects. If hashCode() is wrong, the key might land in the wrong bucket and never be found again even though equals() would say it matches. If equals() is wrong, two objects with the same hash could be treated as the same key when they shouldn't be.
Extending Thread means your class can never extend anything else, since Java only allows single inheritance, and you're tying your business logic directly to the mechanics of thread creation. Implementing Runnable keeps your task logic as a plain class that describes what to run, and you hand that Runnable to a Thread (or, better, an ExecutorService) separately.
class PrintTask implements Runnable {
public void run() { System.out.println("running"); }
}
new Thread(new PrintTask()).start();
// or, preferably, submit it to a thread pool instead of a raw Thread
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(new PrintTask());Runnable is the better default almost always, both because it doesn't burn your one shot at inheritance and because it decouples the task from how it gets executed, letting you swap in a thread pool later without touching the task class itself.
synchronized ensures only one thread at a time can execute a given block or method on a given object, by acquiring that object's intrinsic lock before entering and releasing it on exit, even if an exception is thrown. It's what prevents two threads from corrupting shared state through a classic race condition.
class Counter {
private int count = 0;
synchronized void increment() { count++; }
}The cost is that synchronized blocks serialize execution, only one thread runs the protected code at a time, everyone else waits. Overusing it on code paths that don't actually need protection turns a multi-core program into something that behaves like single-threaded code, and it's easy to accidentally hold a lock across an expensive operation (a network call, a file read) and stall every other thread waiting on it for far longer than necessary.
Comparable is implemented by the class itself and defines one natural ordering through compareTo(), used by Collections.sort() and TreeSet by default. Comparator is a separate object that defines an ordering externally, through compare(), which lets you sort the same class multiple different ways without touching the class's own code.
class Employee implements Comparable<Employee> {
int age;
public int compareTo(Employee other) { return this.age - other.age; }
}
List<Employee> employees = new ArrayList<>();
Collections.sort(employees); // uses natural ordering, by age
employees.sort(Comparator.comparing((Employee e) -> e.name));
// sorts by name instead, without changing Employee at allVertical scaling means adding more resources, CPU, RAM, to a single machine. Horizontal scaling means adding more machines and distributing load across them. Vertical scaling is simpler, no distributed-systems complexity, but it hits a hard ceiling, there's a biggest machine you can buy, and a single point of failure the whole time.
Horizontal scaling has effectively no ceiling since you can keep adding machines, but it introduces real complexity: you now need a load balancer, you need to handle data consistency across nodes, and any state that used to live safely in one process's memory now has to be shared or replicated somehow. Most systems start vertical because it's cheap and simple, and move horizontal once a single machine genuinely can't keep up or once availability requirements demand redundancy anyway.
A monolith deploys the entire application as one unit, all the modules share one codebase, one build, one deployment pipeline, and typically one database. Microservices split the application into independently deployable services, each usually owning its own data store, communicating over the network rather than through in-process function calls.
The tradeoff isn't "microservices are more modern so they're better." A monolith is dramatically simpler to develop, test, and deploy for a small team, and you get transactional consistency almost for free since everything shares one database. Microservices buy you independent deployability and the ability to scale specific pieces separately, at the direct cost of network latency between services, distributed transaction headaches, and a much bigger operational surface, more services to monitor, deploy, and keep compatible with each other's API contracts.
Normalization organizes a relational schema to reduce data redundancy and avoid update anomalies. First normal form (1NF) requires every column to hold atomic, indivisible values, no comma-separated lists crammed into a single field. Second normal form (2NF) requires 1NF plus every non-key column to depend on the entire primary key, not just part of a composite key. Third normal form (3NF) requires 2NF plus no non-key column depending on another non-key column, only on the primary key directly.
-- violates 3NF: department_name depends on department_id,
-- not directly on the employee's primary key
CREATE TABLE employees_bad (
emp_id INT PRIMARY KEY,
department_id INT,
department_name VARCHAR(50)
);
-- normalized: department info moved to its own table
CREATE TABLE departments (
department_id INT PRIMARY KEY,
department_name VARCHAR(50)
);
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
department_id INT REFERENCES departments(department_id)
);An inner join returns only rows where the join condition matches in both tables, anything without a match on either side disappears from the result. A left join keeps every row from the left table regardless of whether a match exists in the right table, filling in NULLs for the right table's columns when there's no match.
SELECT c.name, o.order_id
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id;
-- returns every customer, even ones with zero orders (order_id is NULL for them)
SELECT c.name, o.order_id
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id;
-- returns only customers who have placed at least one orderA customer with no orders shows up once with a NULL order_id in the left join, and doesn't show up at all in the inner join. That single difference is what most reporting bugs about "missing customers" in a dashboard trace back to.
This question is checking whether you can hold a technical position without either steamrolling the other person or silently going along with something you think is wrong. The strongest answers describe a specific disagreement, what each side's reasoning actually was, and how it got resolved, ideally with a concrete criterion (a benchmark, a deadline constraint, a maintainability tradeoff) rather than just "I convinced them" or "I deferred to them."
It's fine, and often more credible, if the resolution wasn't a clean win for your original position. Saying "I pushed for approach A, we prototyped both in an afternoon, and B turned out faster for our actual data size, so we went with B" shows you care more about the right outcome than about being right.
Don't just agree to avoid friction, and don't argue past the point of being useful either. Walk through your reasoning out loud, trace the specific input that would break, and if the interviewer is right, say so plainly and explain the fix. If you genuinely still think your logic is correct, say exactly why, referencing the specific case, rather than repeating your original claim louder.
Interviewers running a technical round are watching how you handle being challenged as much as they're watching the code itself. A candidate who traces through an edge case calmly and either concedes the point or defends it with a concrete counterexample reads as more senior than one who gets defensive or folds immediately without checking.
Hard questions
6Dijkstra's greedily picks the closest unvisited node, relaxes all of its outgoing edges, marks it visited, and repeats until every reachable node has its final shortest distance. A priority queue keyed by current known distance is what keeps it efficient, without one you'd have to scan every unvisited node each iteration to find the closest one.
int[] dijkstra(int n, List<int[]>[] adj, int src) {
int[] dist = new int[n];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[src] = 0;
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
pq.offer(new int[]{src, 0});
while (!pq.isEmpty()) {
int[] cur = pq.poll();
int u = cur[0], d = cur[1];
if (d > dist[u]) continue;
for (int[] edge : adj[u]) {
int v = edge[0], w = edge[1];
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
pq.offer(new int[]{v, dist[v]});
}
}
}
return dist;
}One candidate report specifically mentioned discussing a Dijkstra project during the technical round, so being able to explain both the algorithm and a real system you used it in (routing, network cost modeling, a scheduling problem) is worth having ready.
An LRU cache needs O(1) lookup and O(1) reordering on every access, which rules out a plain array or list on its own. The standard combination is a HashMap for O(1) lookup paired with a doubly linked list that tracks recency, moving an accessed node to the front and evicting from the back when capacity is exceeded.
class LRUCache extends LinkedHashMap<Integer, Integer> {
private final int capacity;
LRUCache(int capacity) {
super(capacity, 0.75f, true);
this.capacity = capacity;
}
int get(int key) {
return super.getOrDefault(key, -1);
}
void put(int key, int value) {
super.put(key, value);
}
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
return size() > capacity;
}
}Java's LinkedHashMap with accessOrder set to true already implements the recency-ordering part for you, which is a legitimate answer in an interview as long as you can also explain how you'd build the same thing from a raw HashMap and doubly linked list if asked to go one level deeper.
The diamond problem happens when a class inherits from two sources that each provide a conflicting implementation of the same method, and the language has no clear rule for which one wins. Java sidesteps this for classes by only allowing single inheritance, but default methods in interfaces reopened the door since a class can implement two interfaces that both declare the same default method.
interface A { default void greet() { System.out.println("A"); } }
interface B { default void greet() { System.out.println("B"); } }
class C implements A, B {
@Override
public void greet() {
A.super.greet(); // must explicitly resolve the conflict
}
}Java forces the implementing class to override the conflicting method and explicitly pick which interface's version to call (or write entirely new logic), rather than silently picking one for you. That explicit resolution is the whole design, the compiler refuses to compile until you've made the choice yourself.
volatile guarantees that a write to a variable by one thread becomes immediately visible to all other threads, by forcing reads and writes to go through main memory instead of a thread's local CPU cache. It doesn't provide atomicity though, count++ on a volatile int is still a read-modify-write with three separate steps, and two threads can still interleave those steps and lose an update.
synchronized gives you both visibility and atomicity for the code inside the block, but it also serializes access, which is heavier than you need if all you actually want is visibility of a simple flag, a boolean shutdown signal one thread sets and another thread polls, for instance. Using volatile there is a legitimate performance choice, but reaching for it on anything more complex than a single flag or reference read is exactly the mistake that causes rare, hard-to-reproduce concurrency bugs.
The advertised average case is O(1), assuming a good hash function spreads keys evenly across buckets so each bucket holds very few entries. The worst case is O(n) if every key collides into the same bucket, since a bucket's entries used to be stored as a plain linked list that has to be scanned linearly.
Java 8 improved the worst case by converting a bucket's linked list into a red-black tree once it grows past a threshold (8 entries by default), which brings the true worst case down to O(log n) for that pathological scenario instead of O(n). The practical takeaway for an interview: O(1) is an average-case claim that depends on hashCode() actually spreading keys well, a poorly written hashCode() that returns the same value for every object turns every HashMap operation into a worst-case scan.
This question is checking whether you can absorb a scope change without either quietly reworking everything from scratch out of frustration or resisting a change that's genuinely necessary. A strong answer names what specifically changed, what part of your existing work stayed reusable versus what had to be thrown out, and how you communicated the impact, timeline, effort, back to whoever asked for the change.
The detail that matters most here is whether you flagged the tradeoff honestly. Saying "the new requirement meant redoing the data model, I told the team it would cost us two extra days, and we agreed to cut a lower-priority feature to make room" shows you treat a requirement change as a negotiation with real costs, not something to silently absorb by working longer hours.
How to prepare for a Virtusa interview
Don't spend your prep time on advanced system design content the way you might for a product company. Multiple candidate reports across different roles converge on the same core: Java fundamentals (OOP, collections, exceptions, multithreading basics), SQL joins and normalization, and enough OS/networking/testing MCQ knowledge to clear the online assessment cleanly. Practice explaining your own past projects out loud, since project discussion eats a real chunk of the technical round and a vague answer here costs more than a shaky algorithm answer.
Because the online assessment is computer-adaptive with sectional locks, do at least one full timed mock of a mixed coding-plus-MCQ test before the real thing, not just isolated LeetCode problems. Getting caught by a section timer you didn't plan for is an avoidable way to lose points on questions you actually know.
Get the reps in before the real thing
Explaining the difference between an abstract class and an interface on paper is different from doing it out loud in 45 seconds while an interviewer waits. LastRoundAI's mock interview mode runs live technical rounds with real-time follow-up questions, and the free plan includes 15 credits a month that reset monthly.
Once your Java and SQL answers hold up under a follow-up question, Auto-Apply queues tailored applications to services and consulting roles 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
What should freshers focus on for a Virtusa 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 Virtusa 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 Virtusa 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 Virtusa 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.
