Software Engineer Interview Questions
Core Overview
Prepare for software engineering interviews covering algorithms, system design, concurrency, testing, delivery practices, and maintainable software development.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What are the main differences between an array-backed list and a linked list?
Direct Answer
Array-backed lists provide fast indexed access, while linked lists require traversal for indexed access. Linked lists can add or remove known nodes efficiently, but use extra memory for links.
Detailed Explanation
An array-backed list stores elements in a contiguous logical sequence and supports efficient positional access.
A linked list stores each element in a node connected to neighboring nodes.
The correct choice depends on the dominant operations. Array-backed lists are usually preferable when indexed reads and iteration are common. Linked lists may help when the application frequently modifies elements through already-known node positions.
Code Example
List<String> arrayBacked = new ArrayList<>();
arrayBacked.add("alpha");
arrayBacked.add("beta");
List<String> linked = new LinkedList<>();
linked.add("alpha");
linked.add("beta");
String value = arrayBacked.get(1);Common Interview Pitfalls
- Assuming linked-list insertion is constant time when the insertion position must first be found.
- Assuming an array-backed list has a permanently fixed capacity.
- Ignoring the additional memory and cache costs of linked-list nodes.
How do hash collisions, capacity, and load factor affect hash-map performance?
Direct Answer
A collision occurs when multiple keys map to the same bucket. Capacity controls the bucket count, while the load factor determines when resizing occurs to limit collisions and preserve efficient access.
Detailed Explanation
A hash map uses a key’s hash value to select a bucket. Different keys may produce the same bucket location, creating a collision. The implementation must then distinguish between the entries stored in that bucket.
Two important configuration values affect performance:
When the number of entries passes the resize threshold, the map increases its capacity and redistributes its entries. This operation can be expensive, but it helps keep buckets from becoming excessively crowded.
A very high load factor reduces memory overhead but may increase collision-related lookup work. A very low load factor can reduce collisions but allocate more memory than necessary.
Hash-map operations are commonly expected to be constant time when hashes are well distributed. That expectation is not an unconditional worst-case guarantee because poor hash distribution or many collisions can increase the amount of work needed.
Code Example
int expectedEntries = 1_000;
float loadFactor = 0.75f;
Map<String, Integer> counts =
new HashMap<>(expectedEntries, loadFactor);
counts.merge("java", 1, Integer::sum);Common Interview Pitfalls
- Claiming that every hash-map lookup is guaranteed to run in constant time.
- Using mutable key fields that change equality or hash-code behavior after insertion.
- Setting a large initial capacity without considering expected entry count and memory use.
What is the difference between a stack, a queue, and a deque?
Direct Answer
A stack processes the most recently added item first, while a queue processes the earliest item first. A deque supports insertion and removal at both ends and can implement either behavior.
Detailed Explanation
These structures differ primarily in where elements are inserted and removed:
A deque can therefore be used as either a stack or a queue:
push, pop, and peek.offer, poll, and peek.Using methods such as offer and poll can be useful because they communicate queue intent and avoid exception-based handling for some normal capacity or empty-queue conditions.
The chosen abstraction should reflect the processing rule required by the problem rather than merely the methods available on a concrete collection.
Code Example
Deque<String> tasks = new ArrayDeque<>();
// Queue behavior
tasks.offerLast("first");
tasks.offerLast("second");
String next = tasks.pollFirst();
// Stack behavior
tasks.push("latest");
String latest = tasks.pop();Common Interview Pitfalls
- Removing from the wrong end of a deque and accidentally reversing the intended order.
- Using a legacy stack implementation when a deque communicates the behavior more clearly.
- Calling methods that throw on an empty structure without handling the empty case.
What precondition does binary search require, and how should its result be interpreted?
Direct Answer
Binary search requires data ordered by the same comparison rule used during the search. It repeatedly halves the search range, and an unsuccessful result may encode the insertion position.
Detailed Explanation
Binary search works by comparing the target with a middle element and discarding the half that cannot contain the target. This produces logarithmic search behavior because the remaining search range is approximately halved after every comparison.
Its essential precondition is that the searched range must already be ordered according to the same natural ordering or comparator used by the search. Searching unsorted data produces an undefined or unreliable result.
In Java’s array search API:
The insertion point can be recovered with -(result + 1).
When duplicate values exist, callers should not assume that an arbitrary binary-search API will return the first or last matching position unless that behavior is explicitly documented or implemented.
Code Example
int[] values = {4, 8, 15, 16, 23, 42};
int result = Arrays.binarySearch(values, 20);
if (result >= 0) {
System.out.println("Found at index " + result);
} else {
int insertionPoint = -(result + 1);
System.out.println("Insert at index " + insertionPoint);
}Common Interview Pitfalls
- Running binary search on data that has not been sorted.
- Sorting with one comparator and searching with a different ordering rule.
- Treating every negative result as minus one instead of decoding the insertion point.
- Assuming the returned index is the first duplicate occurrence.
How does a priority queue differ from a sorted collection?
Direct Answer
A priority queue guarantees efficient access to its highest- or lowest-priority head, but it does not guarantee that iteration returns every element in fully sorted order.
Detailed Explanation
A priority queue organizes elements so that the element with the greatest priority, according to its ordering rule, is available at the head.
The ordering can be defined by:
A heap-backed priority queue maintains only enough structure to identify and remove the head efficiently. It does not maintain every element in a globally sorted sequence for iteration.
In Java’s PriorityQueue implementation:
peek is constant time.offer and removing the head with poll take logarithmic time.Priority queues are useful for scheduling, top-k calculations, graph algorithms, merging ordered streams, and any problem where the next highest-priority item matters more than fully ordering the entire collection.
Code Example
record Task(String name, int priority) {}
PriorityQueue<Task> tasks = new PriorityQueue<>(
Comparator.comparingInt(Task::priority).reversed()
);
tasks.offer(new Task("normal", 1));
tasks.offer(new Task("urgent", 10));
tasks.offer(new Task("important", 5));
Task next = tasks.poll();Common Interview Pitfalls
- Assuming iteration over a priority queue returns elements in priority order.
- Using mutable fields in the comparator and changing them after insertion.
- Removing arbitrary elements repeatedly without considering linear search cost.
- Reversing the comparator incorrectly and processing the lowest-priority item first.
What is a stable sorting algorithm, and when does sort stability matter?
Direct Answer
A stable sort preserves the original relative order of elements that compare as equal. This matters when records are sorted repeatedly by different keys or equal-key ordering carries meaning.
Detailed Explanation
A sorting algorithm is stable when two elements that compare as equal remain in the same relative order they had before sorting.
Suppose employee records are first sorted by name and then stably sorted by department. Employees in the same department retain their prior name order. This allows multiple ordering criteria to be applied in stages, beginning with the least significant key.
Stability is useful when:
Stability is not always required. If equal elements are interchangeable, an unstable algorithm may still satisfy the application’s needs. Candidates should distinguish stability from correctness: an unstable sort can still return elements in valid sorted order, but equal elements may be rearranged.
Code Example
record Candidate(String name, String department) {}
List<Candidate> candidates = new ArrayList<>(List.of(
new Candidate("Asha", "Engineering"),
new Candidate("Ben", "Design"),
new Candidate("Carla", "Engineering")
));
candidates.sort(Comparator.comparing(Candidate::name));
candidates.sort(Comparator.comparing(Candidate::department));Common Interview Pitfalls
- Confusing stable sorting with deterministic sorting.
- Assuming every sorting overload provides the same stability guarantee.
- Applying repeated sorts from the most significant key to the least significant key.
- Ignoring whether equal-key records have an important existing order.
What is the difference between vertical scaling and horizontal scaling?
Direct Answer
Vertical scaling increases the resources of one machine, while horizontal scaling adds more machines or service instances and distributes work among them.
Detailed Explanation
The two approaches increase system capacity in different ways.
Vertical scaling is often simpler because the application may continue running as a single instance. However, one machine has a practical resource limit, upgrades may require downtime, and the machine can remain a single point of failure.
Horizontal scaling can improve both capacity and availability because traffic can be distributed across multiple instances. It usually requires additional design work, including load balancing, service discovery, shared or distributed state, failure handling, and data consistency.
Stateless application servers are generally easier to scale horizontally because any healthy instance can process a request. Stateful components may require replication, partitioning, affinity, or external state storage.
Common Interview Pitfalls
- Assuming horizontal scaling automatically removes every single point of failure.
- Storing important session state only in one application instance.
- Ignoring the operational complexity introduced by distributed instances.
- Assuming vertical scaling can continue without hardware or service limits.
What responsibilities does a load balancer have in a distributed application?
Direct Answer
A load balancer distributes incoming traffic across healthy targets. It may also perform health checks, TLS termination, routing, connection management, and failure isolation.
Detailed Explanation
A load balancer sits between clients and a group of application targets. Its primary responsibility is to distribute incoming requests so that no single target handles all traffic.
Common responsibilities include:
A load balancer does not repair a faulty application or guarantee unlimited capacity. The targets, dependencies, scaling rules, health checks, and failure behavior must still be designed correctly.
Common Interview Pitfalls
- Using health checks that verify only that a process is running.
- Assuming a load balancer can make an unhealthy dependency available.
- Keeping session state on one target without planning for request routing.
- Configuring retries without considering duplicate side effects.
How does the cache-aside pattern work, and why is cache invalidation difficult?
Direct Answer
With cache-aside, the application reads from the cache first and loads missing data from the database. Invalidation is difficult because cached copies can become stale after updates.
Detailed Explanation
In the cache-aside pattern, the application manages the cache explicitly.
A typical read flow is:
1. Read the value from the cache.
2. If it exists, return the cached value.
3. If it is missing, read the value from the database.
4. Store the result in the cache with an appropriate expiration.
5. Return the value to the caller.
The pattern is useful because only requested data is loaded into the cache. The database remains the authoritative source, and the application can still function after a cache miss.
Invalidation is difficult because the same logical data may have multiple cached representations. When the source data changes, the application must decide whether to delete, replace, version, or allow each cached representation to expire.
Common controls include:
The correct strategy depends on how much staleness the application can tolerate and how much load the source database can handle.
Code Example
async function getProduct(productId: string): Promise<Product | null> {
const cacheKey = `product:${productId}`;
const cached = await cache.get(cacheKey);
if (cached !== null) {
return JSON.parse(cached) as Product;
}
const product = await productRepository.findById(productId);
if (product !== null) {
await cache.set(cacheKey, JSON.stringify(product), {
ttlSeconds: 300
});
}
return product;
}Common Interview Pitfalls
- Caching data without defining an expiration or invalidation strategy.
- Caching database misses for too long and hiding newly created records.
- Deleting a cache entry before confirming that the database write succeeded.
- Allowing many concurrent misses to overload the backing database.
What is the difference between database replication and database sharding?
Direct Answer
Replication stores copies of the same data on multiple nodes, while sharding divides different portions of a dataset across nodes using a partitioning rule.
Detailed Explanation
Replication and sharding solve different database scaling and reliability problems.
Replication maintains copies of the same logical data on multiple database nodes. It can improve:
Replication may be synchronous or asynchronous. Asynchronous replicas can lag behind the primary, so a read from a replica may not immediately reflect a recent write.
Sharding partitions a dataset so different records are stored on different database nodes. A shard key determines where each record belongs. Sharding can increase storage and write capacity by distributing data and traffic.
Sharding adds complexity because the system must route requests to the correct shard. Cross-shard joins, transactions, aggregations, rebalancing, and hotspot prevention can also become difficult.
A system may use both techniques: data is divided into shards, and each shard is replicated for availability.
Common Interview Pitfalls
- Treating replication and sharding as interchangeable scaling strategies.
- Assuming read replicas always contain the latest committed data.
- Selecting a shard key that sends most traffic to one shard.
- Ignoring cross-shard queries and transactions during schema design.
What makes an API operation idempotent, and how can a create operation support safe retries?
Direct Answer
An operation is idempotent when repeating the same request has the same intended server effect as sending it once. Create operations can use a unique idempotency key to deduplicate retries.
Detailed Explanation
An API operation is idempotent when multiple identical requests have the same intended effect on server state as one request.
HTTP defines methods such as PUT and DELETE as idempotent by semantics. This does not mean every response must be identical. For example, the first delete may remove a resource while a later delete reports that it is already absent, but the intended final state remains the same.
A create operation commonly uses POST, which is not inherently idempotent. To make retries safer, an API can accept an idempotency key:
1. The client generates a unique key for one logical operation.
2. The server stores the key with the operation result.
3. A retry with the same key returns the stored result or recognizes the operation in progress.
4. The key is scoped to the correct user, endpoint, and request payload.
The server should process the idempotency record and business change atomically or with equivalent coordination. Otherwise, a failure between creating the resource and recording the key can still produce duplicates.
Code Example
POST /api/payments
Idempotency-Key: 8df142dd-879c-4cb0-9f59-c34e118a35da
Content-Type: application/json
{
"orderId": "order-4821",
"amount": 4999,
"currency": "USD"
}Common Interview Pitfalls
- Assuming every POST request is automatically safe to retry.
- Reusing one idempotency key for different logical operations.
- Recording the key separately from the business operation without failure coordination.
- Returning a different resource when the same key is retried.
How should the CAP theorem influence system behavior during a network partition?
Direct Answer
During a network partition, a distributed system cannot guarantee both immediate consistency and availability for every request. It must reject or delay some operations, or accept potentially stale state.
Detailed Explanation
The CAP theorem describes a constraint that becomes important when network communication between parts of a distributed system is disrupted.
The three properties are:
When a partition occurs, the system must choose its behavior for affected operations:
This is not usually one permanent system-wide choice. Different operations can have different requirements. A product catalog may tolerate stale reads, while a uniqueness constraint, inventory reservation, or financial transfer may require stronger coordination.
The interview answer should connect the tradeoff to specific business invariants, failure behavior, reconciliation, and user-visible consequences rather than simply labelling a database as CP or AP.
Common Interview Pitfalls
- Describing CAP as a choice of any two properties during normal operation.
- Assuming eventual consistency means that conflicting writes resolve automatically.
- Applying one consistency policy to every operation without business analysis.
- Ignoring how clients observe errors, stale reads, or reconciliation.
What is the difference between a process and a thread?
Direct Answer
A process is an independently executing program with its own resources, while threads are execution paths within a process that share its memory and resources.
Detailed Explanation
A process is an independently executing program instance. Processes normally have separate memory spaces and operating-system-managed resources.
A thread is a path of execution within a process. Multiple threads in the same process typically share:
Each thread still maintains its own execution state, including its stack, program counter, and local method variables.
Threads can communicate efficiently because they share memory, but shared access also creates risks such as race conditions, visibility problems, and deadlocks. Communication between separate processes generally requires an explicit mechanism such as sockets, pipes, shared memory, or messaging.
Creating and switching between threads is commonly less expensive than creating and switching between processes, but a failure involving shared process state can affect all threads in that process.
Code Example
Thread worker = Thread.ofPlatform().start(() -> {
System.out.println(
"Running in: " + Thread.currentThread().getName()
);
});
worker.join();Common Interview Pitfalls
- Claiming that threads within one process have completely separate memory.
- Assuming shared memory automatically makes thread communication safe.
- Using the terms process and thread as if they mean the same thing.
- Ignoring that one process can contain many concurrent threads.
What is a race condition, and what is a critical section?
Direct Answer
A race condition occurs when a result depends on unpredictable thread interleaving. A critical section is code that accesses shared state and must be coordinated.
Detailed Explanation
A race condition occurs when multiple threads access shared state and the program’s result depends on the order or timing of their operations.
For example, an increment such as count++ is not necessarily one indivisible action. It can involve:
1. Reading the current value.
2. Calculating the new value.
3. Writing the updated value.
If two threads perform these steps concurrently, both may read the same original value and one update may be lost.
A critical section is the part of a program that reads or modifies shared state and therefore requires controlled access. Common protection mechanisms include:
synchronized blocks or methodsSynchronization should protect the complete invariant or compound operation, not only one individual read or write. The smallest practical critical section is usually preferable because unnecessarily broad locking can reduce concurrency.
Code Example
final class Counter {
private int value;
public synchronized void increment() {
value++;
}
public synchronized int get() {
return value;
}
}Common Interview Pitfalls
- Assuming that a simple increment is always an atomic operation.
- Protecting writes while leaving related reads unsynchronized.
- Using different locks to protect the same shared invariant.
- Making the critical section much larger than necessary.
How do mutexes, locks, and semaphores differ?
Direct Answer
A mutex provides exclusive ownership of a resource, a lock is a broader coordination abstraction, and a semaphore uses permits to limit concurrent access.
Detailed Explanation
A mutex is a mutual-exclusion mechanism that permits only one thread to own access to a protected resource at a time.
A lock is a broader programming abstraction used to coordinate access to shared state. A lock may provide capabilities such as:
Java’s ReentrantLock, for example, provides mutual exclusion similar to a synchronized monitor but with additional acquisition and condition-management options.
A semaphore maintains a number of permits. A thread acquires a permit before entering a limited resource and releases it afterward. A semaphore initialized with one permit can resemble mutual exclusion, but semaphores do not necessarily express the same ownership discipline as a mutex.
Semaphores are useful for limiting access to resources such as database connections, external API capacity, or a fixed number of concurrent operations.
Code Example
Semaphore permits = new Semaphore(3);
void callExternalService() throws InterruptedException {
permits.acquire();
try {
externalService.call();
} finally {
permits.release();
}
}Common Interview Pitfalls
- Failing to release a lock or permit inside a finally block.
- Releasing more semaphore permits than were acquired.
- Using a semaphore when exclusive ownership semantics are required.
- Assuming a fair lock guarantees operating-system thread scheduling fairness.
What causes a deadlock, and how can it be prevented?
Direct Answer
Deadlock occurs when threads wait indefinitely for resources held by one another. Consistent lock ordering, timeouts, reduced lock scope, and avoiding nested locks help prevent it.
Detailed Explanation
A deadlock occurs when two or more threads are permanently blocked because each is waiting for a resource held by another thread in the cycle.
A common example is:
The commonly discussed conditions associated with deadlock are:
Prevention strategies include:
tryLock and backing off after failure.Code Example
void transfer(Account first, Account second) {
Account lower =
first.id() < second.id() ? first : second;
Account higher =
first.id() < second.id() ? second : first;
synchronized (lower) {
synchronized (higher) {
performTransfer(first, second);
}
}
}Common Interview Pitfalls
- Acquiring the same set of locks in different orders across code paths.
- Holding a lock while performing slow network or file operations.
- Assuming synchronized code cannot deadlock.
- Using a timeout without handling partial work or retry behavior.
Why use a thread pool or ExecutorService instead of creating a new platform thread for every task?
Direct Answer
Executors separate task submission from execution policy. Thread pools reuse workers, limit concurrency, queue work, manage shutdown, and reduce thread-creation overhead.
Detailed Explanation
An executor separates what work should run from how and where that work is executed.
An ExecutorService can provide:
Future results and cancellationCreating an unrestricted platform thread for every task can consume significant memory and scheduling resources under heavy load. A bounded execution strategy can protect the application and its downstream dependencies from excessive concurrency.
Thread-pool configuration should consider:
Java 21 also supports virtual-thread-per-task executors. Virtual threads can make thread-per-task code practical for many blocking I/O workloads, but they do not remove downstream capacity limits or the need to control access to scarce resources.
Code Example
ExecutorService executor =
Executors.newFixedThreadPool(4);
try {
Future<Integer> result = executor.submit(() -> {
return calculateValue();
});
System.out.println(result.get());
} finally {
executor.shutdown();
}Common Interview Pitfalls
- Creating an unbounded number of platform threads under heavy load.
- Using an unbounded task queue without considering memory growth.
- Forgetting to shut down an executor owned by the application.
- Assuming virtual threads remove database or network capacity limits.
- Blocking indefinitely on Future.get without timeout or cancellation planning.
What is a happens-before relationship, and why is it important for memory visibility?
Direct Answer
A happens-before relationship guarantees that one thread’s earlier effects are visible to another ordered action. Without it, shared reads may observe stale or reordered values.
Detailed Explanation
The Java Memory Model uses happens-before relationships to define when the effects of one action are guaranteed to be visible to another action.
Important examples include:
volatile field happens-before a subsequent read of that field.Thread.start() happens-before actions performed by the started thread.join() on it.Happens-before provides ordering and visibility guarantees. It does not necessarily mean that actions must physically execute in that exact order at the processor level. Compilers and processors may reorder operations when the resulting execution remains valid under the memory model.
Without an appropriate happens-before edge, conflicting access to shared mutable data can form a data race. One thread may then observe stale values, partially coordinated state, or behavior that does not match simple source-code ordering assumptions.
volatile provides visibility and ordering for accesses to that field, but it does not make a compound operation such as value++ atomic.
Code Example
final class TaskState {
private int result;
private volatile boolean complete;
void produce() {
result = 42;
complete = true;
}
int consume() {
if (!complete) {
throw new IllegalStateException("Not complete");
}
return result;
}
}Common Interview Pitfalls
- Assuming source-code order alone guarantees cross-thread visibility.
- Using volatile for a compound read-modify-write operation.
- Confusing atomicity with memory visibility.
- Publishing a mutable object without a safe publication mechanism.
- Assuming sleep creates a happens-before relationship.
What is the difference between a unit test and an integration test?
Direct Answer
A unit test checks a small component in isolation, while an integration test verifies that multiple components or external systems work together correctly.
Detailed Explanation
A unit test verifies a small piece of behavior, such as a function, class, or domain rule, with dependencies controlled or replaced when appropriate.
Unit tests are generally:
An integration test verifies cooperation between multiple parts of the system. It may involve a database, HTTP layer, message broker, filesystem, framework configuration, or several real application components.
Integration tests provide confidence that boundaries and configurations work correctly, but they are usually slower and require more setup than unit tests.
The distinction is based on test scope and dependency boundaries, not only on the testing framework used. The same framework can run both unit and integration tests.
Code Example
class PriceCalculatorTest {
@Test
void appliesPercentageDiscount() {
PriceCalculator calculator = new PriceCalculator();
BigDecimal result = calculator.applyDiscount(
new BigDecimal("100.00"),
new BigDecimal("0.20")
);
assertEquals(new BigDecimal("80.0000"), result);
}
}Common Interview Pitfalls
- Calling every automated test a unit test regardless of its dependencies.
- Replacing all collaborators with mocks and testing implementation details.
- Using a real database in a test intended to be fast and isolated.
- Relying only on unit tests without testing important integration boundaries.
What are mocks, stubs, and fakes, and when should each be used?
Direct Answer
A stub returns controlled responses, a mock also verifies interactions, and a fake provides a simplified working implementation such as an in-memory repository.
Detailed Explanation
Test doubles replace real dependencies so a test can control inputs, isolate behavior, or avoid slow and unreliable external systems.
For example, a payment-service test might use:
Test doubles should support meaningful behavior testing. Excessive interaction verification can tightly couple tests to implementation details and make safe refactoring unnecessarily difficult.
Code Example
PaymentGateway gateway = mock(PaymentGateway.class);
when(gateway.charge(4999)).thenReturn(
new PaymentResult("payment-123", true)
);
CheckoutService service = new CheckoutService(gateway);
Receipt receipt = service.checkout(4999);
assertTrue(receipt.successful());
verify(gateway).charge(4999);Common Interview Pitfalls
- Using the words mock and stub interchangeably in every context.
- Verifying every internal method call instead of observable behavior.
- Creating complicated mocks that reproduce the production implementation.
- Using a fake whose behavior differs materially from the real dependency.
What is the test pyramid, and how should it influence a testing strategy?
Direct Answer
The test pyramid recommends many fast, focused lower-level tests, fewer integration tests, and a small number of broad end-to-end tests for critical workflows.
Detailed Explanation
The test pyramid is a model for balancing automated tests at different scopes.
A typical interpretation includes:
The purpose is not to enforce an exact percentage. It is to avoid relying primarily on slow, brittle, broad tests when the same behavior can be verified more reliably at a lower level.
A practical strategy considers:
Teams should place each assertion at the lowest test level that can provide meaningful confidence while still retaining enough higher-level tests to verify integration and deployment behavior.
Common Interview Pitfalls
- Treating the test pyramid as a mandatory percentage formula.
- Testing every possible behavior through the user interface.
- Having many unit tests but no coverage of important system boundaries.
- Duplicating the same assertions at every test level.
- Ignoring test maintenance cost when adding broad end-to-end tests.
What makes a test flaky, and how can flaky tests be prevented or diagnosed?
Direct Answer
A flaky test passes and fails without a relevant code change. Common causes include timing, shared state, test ordering, unreliable dependencies, randomness, and concurrency.
Detailed Explanation
A flaky test produces inconsistent results even though the application code relevant to the test has not changed.
Common causes include:
Ways to improve determinism include:
Code Example
@Test
void tokenIsExpiredAfterDeadline() {
Instant now = Instant.parse("2026-08-04T12:00:00Z");
Clock clock = Clock.fixed(now, ZoneOffset.UTC);
Token token = new Token(
now.minusSeconds(120),
Duration.ofSeconds(60)
);
assertTrue(token.isExpired(clock));
}Common Interview Pitfalls
- Adding longer sleep calls instead of waiting for an observable condition.
- Automatically retrying flaky tests without investigating their cause.
- Allowing tests to depend on execution order.
- Using shared accounts or records across parallel test runs.
- Quarantining a flaky test permanently and forgetting its coverage.
What stages and quality gates should a CI/CD pipeline contain?
Direct Answer
A pipeline commonly validates source code, builds an immutable artifact, runs automated checks, scans for risks, deploys safely, and verifies the deployed release.
Detailed Explanation
A CI/CD pipeline automates the path from a source-code change to a validated release.
Common stages include:
A quality gate is a condition that must pass before later work proceeds. Examples include successful tests, approved reviews, acceptable vulnerability findings, or a healthy staging deployment.
Fast checks should normally run early so developers receive useful feedback quickly. Independent jobs can run in parallel, while later deployment stages should consume the same artifact that earlier stages tested.
Code Example
name: Verify application
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run buildCommon Interview Pitfalls
- Rebuilding a different artifact for production than the one previously tested.
- Running fast validation only near the end of a long pipeline.
- Allowing failed quality gates to be routinely ignored.
- Placing secrets directly inside pipeline configuration.
- Deploying successfully without verifying application health afterward.
How do progressive delivery and rollback reduce production deployment risk?
Direct Answer
Progressive delivery exposes a release gradually while monitoring health. Rollback restores a known-good version when predefined signals show unacceptable impact.
Detailed Explanation
Progressive delivery limits the number of users or instances exposed to a new release before it is promoted broadly.
Common approaches include:
A safe rollout should define measurable success and failure criteria before deployment. Useful signals can include:
A rollback returns traffic or workloads to a known-good application version. However, application rollback may not safely reverse destructive database migrations or irreversible external side effects.
Backward-compatible database changes, expand-and-contract migrations, tested rollback procedures, immutable artifacts, and observability are therefore important parts of deployment safety.
Code Example
apiVersion: apps/v1
kind: Deployment
metadata:
name: interview-api
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
selector:
matchLabels:
app: interview-api
template:
metadata:
labels:
app: interview-api
spec:
containers:
- name: api
image: registry.example.com/interview-api:2.4.0
readinessProbe:
httpGet:
path: /ready
port: 8080Common Interview Pitfalls
- Calling a deployment successful before observing application and business metrics.
- Assuming application rollback also reverses every database change.
- Deploying all instances simultaneously without preserving healthy capacity.
- Using a readiness check that does not represent the ability to serve traffic.
- Starting a canary rollout without predefined promotion or rollback criteria.
Why are meaningful names and consistent coding conventions important?
Direct Answer
Meaningful names communicate intent, while consistent conventions make code easier to read, review, search, maintain, and modify without repeatedly interpreting local styles.
Detailed Explanation
Names are part of a program’s design because they communicate the purpose of classes, methods, variables, and domain concepts.
A useful name should generally reveal:
For example, elapsedTimeInSeconds communicates more than value, while findActiveSubscription communicates both the action and the expected result.
Consistent coding conventions also reduce unnecessary variation. When formatting, capitalization, file organization, and naming patterns are predictable, reviewers can focus on behavior and design rather than style disagreements.
Names should not compensate for an unclear design. If a method requires an unusually long explanation in its name, it may be doing too much or combining several responsibilities.
Code Example
// Unclear
int d;
List<User> list = load();
// Clearer
int retryDelaySeconds;
List<User> activeUsers = loadActiveUsers();Common Interview Pitfalls
- Using generic names such as data, value, item, or manager without context.
- Encoding implementation details into names that may soon become outdated.
- Using unexplained abbreviations that are not standard in the domain.
- Choosing a misleading name and relying on comments to correct it.
- Applying naming conventions inconsistently within the same codebase.
What are the SOLID principles, and what problems are they intended to reduce?
Direct Answer
SOLID describes five object-oriented design principles intended to improve cohesion, substitutability, extensibility, interface focus, and dependency decoupling.
Detailed Explanation
SOLID is a group of object-oriented design principles used to reason about responsibilities and dependencies.
These principles are guidelines rather than mechanical rules. Applying them without considering the size and volatility of the system can create unnecessary interfaces, indirection, and complexity.
A strong design applies separation only where it improves changeability, testability, or understanding. A small stable component may not need several abstraction layers merely to appear compliant.
Code Example
interface PaymentGateway {
PaymentResult charge(Money amount);
}
final class CheckoutService {
private final PaymentGateway paymentGateway;
CheckoutService(PaymentGateway paymentGateway) {
this.paymentGateway = paymentGateway;
}
PaymentResult checkout(Money total) {
return paymentGateway.charge(total);
}
}Common Interview Pitfalls
- Treating SOLID as a requirement to create an interface for every class.
- Confusing single responsibility with having only one method.
- Using inheritance even when the subtype cannot preserve the parent contract.
- Splitting interfaces so aggressively that the design becomes difficult to navigate.
- Adding abstraction before a meaningful source of variation exists.
What is a code smell, and how should an engineer refactor code safely?
Direct Answer
A code smell is a design warning rather than a confirmed defect. Safe refactoring improves internal structure in small steps while preserving observable behavior.
Detailed Explanation
A code smell is an indicator that code may be difficult to understand, change, test, or extend. It is not automatically a bug and must be evaluated in context.
Examples include:
Refactoring changes the internal structure of code without intentionally changing its externally observable behavior.
A safe process normally includes:
1. Establishing tests or other checks for the current behavior.
2. Making one small structural change.
3. Running the relevant verification.
4. Committing the change independently when practical.
5. Repeating until the design is clearer.
Refactoring should generally be separated from feature changes when possible. Combining major restructuring with new behavior makes review, testing, and rollback more difficult.
Code Example
// Before
Money calculateTotal(Order order) {
Money total = Money.zero();
for (LineItem item : order.items()) {
total = total.add(
item.unitPrice().multiply(item.quantity())
);
}
return total;
}
// After
Money calculateTotal(Order order) {
return order.items()
.stream()
.map(LineItem::subtotal)
.reduce(Money.zero(), Money::add);
}Common Interview Pitfalls
- Treating every code smell as proof that the implementation is incorrect.
- Refactoring without tests or another way to verify preserved behavior.
- Combining a large refactor with unrelated feature development.
- Introducing a design pattern without a problem that requires it.
- Changing public behavior while describing the work as only refactoring.
How should defensive programming and input validation be applied without hiding defects?
Direct Answer
Validate untrusted data at system boundaries, enforce domain invariants internally, fail clearly on invalid states, and avoid catching errors merely to continue with corrupted assumptions.
Detailed Explanation
Defensive programming anticipates invalid inputs, dependency failures, and violated assumptions while keeping failures understandable.
At an external boundary, validation should consider:
Validation should occur server-side even when the client also validates. Client validation improves usability, but the client is not a trusted enforcement boundary.
Internally, constructors, value objects, and domain methods can preserve invariants so invalid states are harder to represent.
Defensive programming does not mean catching every exception and continuing. Unexpected failures should be logged with appropriate context, translated at clear boundaries, and allowed to fail safely when recovery is not possible.
Code Example
public record CreateAccountRequest(
String email,
int age
) {
public CreateAccountRequest {
if (email == null || email.isBlank()) {
throw new IllegalArgumentException(
"Email is required"
);
}
if (age < 18 || age > 120) {
throw new IllegalArgumentException(
"Age is outside the supported range"
);
}
}
}Common Interview Pitfalls
- Trusting client-side validation as the only security control.
- Using a denylist when a narrow allowlist is practical.
- Catching all exceptions and returning a successful result.
- Logging passwords, access tokens, or other sensitive input values.
- Repeating inconsistent validation rules across several layers.
- Silently replacing invalid data with misleading default values.
What should engineers evaluate during a code review?
Direct Answer
A code review should evaluate correctness, design, complexity, tests, security, naming, documentation, maintainability, and whether the change fits the surrounding system.
Detailed Explanation
Code review is a quality and knowledge-sharing activity, not merely a search for formatting problems.
A reviewer should evaluate:
Smaller, focused changes are generally easier to understand and review thoroughly. Feedback should explain the underlying concern and distinguish blocking correctness issues from optional suggestions.
Approval should not mean that the code is perfect. It should mean the change improves or maintains the health of the codebase and is safe enough to proceed.
Common Interview Pitfalls
- Reviewing only formatting while ignoring design and correctness.
- Approving a change without understanding its intended behavior.
- Requesting broad unrelated refactoring in a focused change.
- Writing vague feedback without explaining the technical concern.
- Treating personal preferences as mandatory project standards.
- Ignoring tests, migrations, observability, or deployment impact.
How should a team manage technical debt and communicate architectural tradeoffs?
Direct Answer
Teams should document debt, measure its impact, prioritize it against product risk, and record architectural decisions with assumptions, alternatives, benefits, costs, and review triggers.
Detailed Explanation
Technical debt represents the future cost created by a design, implementation, operational, or delivery decision. Some debt is deliberate and may be reasonable when speed is important, while other debt emerges unintentionally through incomplete understanding or repeated short-term fixes.
Useful management practices include:
Architectural choices nearly always involve tradeoffs. For example, caching may reduce latency while increasing invalidation complexity. Replication may improve availability while introducing consistency considerations.
A decision record should capture:
Calling every disliked design “technical debt” is unhelpful. The concern should be linked to a concrete cost, risk, or limitation.
Common Interview Pitfalls
- Treating all technical debt as evidence of poor engineering.
- Creating debt tickets without impact, ownership, or review criteria.
- Postponing all remediation until a large rewrite becomes necessary.
- Choosing architecture based only on technical elegance.
- Failing to revisit decisions after assumptions or usage patterns change.
- Describing a tradeoff without identifying who or what bears the cost.
Official Documentation & Specifications
Data Structures & Algorithms
System Design
- AWS Reliable Scalability Guidance
- AWS Well-Architected Scaling Guidance
- AWS Elastic Load Balancing Overview
- AWS Application Load Balancer
- AWS Load Balancing Best Practice
- AWS Database Caching Patterns
- AWS Caching Best Practice
- Amazon ElastiCache Strategies
- Google Cloud Database Sharding
- Google Cloud Spanner Replication
- Google Cloud Replication Lag
- RFC 9110 HTTP Semantics
- RFC 9112 HTTP 1.1
- AWS CAP Theorem Guidance
- AWS Distributed System Availability
Concurrency & Threading
- Oracle Processes and Threads
- Java Thread Documentation
- Oracle Thread Interference
- Oracle Synchronization Overview
- Oracle Intrinsic Locks
- Java Lock Interface
- Java ReentrantLock Documentation
- Java Semaphore Documentation
- Oracle Deadlock Tutorial
- Oracle Concurrency Liveness
- Oracle Lock Objects
- Java ExecutorService Documentation
- Java ThreadPoolExecutor Documentation
- Oracle Java Virtual Threads
- Java Language Specification Threads and Locks
- Java Concurrent Package Memory Effects
- Oracle Memory Consistency Errors
Testing & CI/CD
- JUnit User Guide
- Mockito Documentation
- Mockito Stubbing Reference
- GitLab Testing Strategy
- GitLab Unhealthy Tests
- GitLab Testing Best Practices
- GitLab Test Quarantine
- GitHub Continuous Integration
- GitHub Continuous Deployment
- GitLab CI CD Pipelines
- GitLab CI CD Jobs
- Kubernetes Deployments
- Kubernetes Workload Management
- Kubernetes Health Probes
Coding Standards & Best Practices
- Google Java Style Guide
- Google Code Review Guidance
- Microsoft Architectural Principles
- Microsoft Microservice Application Design
- OWASP Input Validation Guidance
- OWASP REST Security Guidance
- OWASP Error Handling Guidance
- Google Code Review Guidelines
- Google Code Review Standard
- Google Small Changes Guidance
- AWS Technical Debt Guidance
- AWS Architecture Tradeoff Guidance
- AWS Decision Tradeoff Guidance
- AWS Well-Architected Framework
Want to tailer your resume for Software Engineer roles?
Import your resume, scan it for critical Software Engineer keywords, and compare it against ATS standards instantly.