Software Developer Interview Questions
Core Overview
Prepare for software developer interviews covering object-oriented design, databases, API development, maintainable code, and systematic debugging.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What is encapsulation, and how does it help an object preserve valid state?
Direct Answer
Encapsulation hides internal representation and exposes controlled operations, allowing an object to validate changes and preserve its business invariants.
Detailed Explanation
Encapsulation combines data with the operations that manage it while limiting direct access to the internal representation.
A well-encapsulated object exposes behavior instead of allowing callers to modify every field freely. This helps the object enforce its invariants, which are conditions that must remain true throughout its valid lifetime.
For example, a bank account may require that:
If callers can assign the balance directly, those rules can be bypassed. A controlled method such as withdraw can validate the request before changing state.
Encapsulation does not mean making every field private while exposing unrestricted getters and setters. If a setter allows any value, the object may still fail to protect its invariants. The public API should represent valid domain operations.
Code Example
public final class BankAccount {
private BigDecimal balance;
public BankAccount(BigDecimal openingBalance) {
if (openingBalance.signum() < 0) {
throw new IllegalArgumentException(
"Opening balance cannot be negative"
);
}
this.balance = openingBalance;
}
public void withdraw(BigDecimal amount) {
if (amount.signum() <= 0) {
throw new IllegalArgumentException(
"Withdrawal must be positive"
);
}
if (balance.compareTo(amount) < 0) {
throw new IllegalStateException(
"Insufficient funds"
);
}
balance = balance.subtract(amount);
}
public BigDecimal balance() {
return balance;
}
}Common Interview Pitfalls
- Equating encapsulation only with declaring fields private.
- Providing unrestricted setters that allow invalid object states.
- Returning mutable internal collections directly to callers.
- Placing domain validation only in controllers or user interfaces.
When should composition be preferred over inheritance?
Direct Answer
Prefer composition when one object needs another object’s capabilities without forming a genuine substitutable subtype relationship or inheriting implementation details.
Detailed Explanation
Inheritance models an is-a relationship: a subtype should satisfy the behavioral contract expected from its parent type.
Composition models a has-a or uses-a relationship: an object delegates part of its behavior to another object.
Composition is often preferable when:
Inheritance can create tight coupling because subclasses depend on protected state, lifecycle rules, and implementation choices of the parent. A parent-class change can unexpectedly affect subclasses.
Inheritance remains appropriate when the subtype is genuinely substitutable for the parent and the hierarchy expresses a stable domain relationship. The decision should be based on behavioral contracts, not merely on avoiding duplicated code.
Code Example
interface DiscountPolicy {
Money apply(Money subtotal);
}
final class CheckoutService {
private final DiscountPolicy discountPolicy;
CheckoutService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
Money calculateTotal(Money subtotal) {
return discountPolicy.apply(subtotal);
}
}Common Interview Pitfalls
- Using inheritance only to reuse a small amount of implementation.
- Creating a subtype that cannot satisfy the parent type contract.
- Building deep inheritance hierarchies that are difficult to change.
- Assuming composition always requires a dependency injection framework.
How do interfaces and abstract classes differ, and when should each be used?
Direct Answer
Interfaces define capabilities and contracts across unrelated types, while abstract classes can share state, constructors, protected behavior, and partial implementation within a hierarchy.
Detailed Explanation
An interface primarily defines a contract that implementing types agree to satisfy. A class can implement multiple interfaces, allowing capabilities to be composed across otherwise unrelated class hierarchies.
An abstract class participates in a class inheritance hierarchy and can provide:
Use an interface when:
Use an abstract class when:
Modern interfaces can contain default and static methods, but they still should not become containers for unrelated implementation merely to avoid creating a class.
Code Example
interface Auditable {
AuditEntry auditEntry();
}
abstract class Document {
private final String id;
protected Document(String id) {
this.id = id;
}
public String id() {
return id;
}
public abstract String render();
}
final class Invoice extends Document
implements Auditable {
Invoice(String id) {
super(id);
}
@Override
public String render() {
return "Invoice " + id();
}
@Override
public AuditEntry auditEntry() {
return new AuditEntry(id(), "invoice");
}
}Common Interview Pitfalls
- Using an abstract class only because two types contain similar code.
- Adding unrelated default methods to an interface for implementation reuse.
- Exposing protected mutable state that subclasses can invalidate.
- Creating an interface with only one implementation without a variation boundary.
How do polymorphism, method overriding, and method overloading differ?
Direct Answer
Overriding supplies subtype behavior selected at runtime, while overloading defines methods with different parameter lists selected from compile-time argument types.
Detailed Explanation
Polymorphism allows code to work through a common type while different implementations provide type-specific behavior.
Method overriding occurs when a subclass or implementing class provides an instance method matching an inherited method contract. The implementation invoked is selected dynamically according to the runtime object.
Method overloading occurs when several methods share a name but have different parameter lists. Overload selection is performed using compile-time types and applicable conversion rules.
For example, a variable declared as NotificationSender can reference an EmailSender. Calling send invokes the implementation belonging to the runtime object. That is overriding and runtime polymorphism.
By contrast, methods such as format(int) and format(String) are overloads. The compiler selects an overload from the declared argument types.
Return type alone cannot distinguish overloaded methods. Static methods are hidden rather than overridden, so their selection does not use ordinary runtime polymorphism.
Code Example
interface NotificationSender {
void send(String message);
}
final class EmailSender
implements NotificationSender {
@Override
public void send(String message) {
System.out.println("Email: " + message);
}
}
final class Formatter {
String format(int value) {
return Integer.toString(value);
}
String format(String value) {
return value.trim();
}
}
NotificationSender sender = new EmailSender();
sender.send("Welcome");Common Interview Pitfalls
- Describing method overloading as runtime polymorphism.
- Attempting to overload methods using only different return types.
- Accidentally changing a parameter type instead of overriding a method.
- Assuming static methods do not participate in dynamic dispatch.
- Using an unsafe narrower contract in an overriding implementation.
What contract must equals and hashCode satisfy, and why does it matter?
Direct Answer
Objects considered equal must produce the same hash code. Equality should also be reflexive, symmetric, transitive, consistent, and false for null.
Detailed Explanation
The equals method defines logical equality, while hashCode supplies a value used by hash-based collections such as HashMap and HashSet.
The equality contract requires that equals be:
a equals b, then b equals a.a equals b and b equals c, then a equals c.null.The hash-code contract requires that equal objects return the same hash code. Unequal objects may still have the same hash code because collisions are allowed.
Fields used for equality should generally remain stable while an object is stored in a hash-based collection. If those fields change, the collection may search the wrong bucket and fail to locate the object correctly.
Records automatically derive component-based equals and hashCode, making them useful for many immutable data aggregates.
Code Example
public record ProductId(String value) {
public ProductId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException(
"Product ID is required"
);
}
}
}Common Interview Pitfalls
- Overriding equals without providing a compatible hashCode implementation.
- Assuming unequal objects must always have different hash codes.
- Using mutable fields for equality while objects are stored in a HashSet.
- Implementing equality asymmetrically across a class hierarchy.
- Comparing reference identity when logical value equality is required.
How does dependency injection improve object-oriented design, and what lifetime problems can it introduce?
Direct Answer
Dependency injection supplies collaborators externally, reducing construction coupling. Incorrect lifetimes can cause shared-state bugs, stale data, leaks, or short-lived services captured by long-lived ones.
Detailed Explanation
Dependency injection means that an object receives the collaborators it needs instead of constructing concrete dependencies internally.
This can improve design by:
Constructor injection is often preferred for required dependencies because it makes invalid or incomplete construction more difficult.
A dependency injection container may manage service lifetimes such as:
A long-lived service should not directly capture a shorter-lived dependency. For example, a singleton holding a request-scoped repository can retain request-specific state beyond its valid lifetime.
Singleton services that contain mutable state must also be thread-safe. Dependency injection does not automatically make a design decoupled or correct; depending on a large service locator or broad container API can hide dependencies and recreate coupling.
Code Example
interface UserRepository {
Optional<User> findById(UserId id);
}
final class UserProfileService {
private final UserRepository repository;
UserProfileService(UserRepository repository) {
this.repository = repository;
}
UserProfile load(UserId id) {
User user = repository.findById(id)
.orElseThrow(UserNotFoundException::new);
return UserProfile.from(user);
}
}Common Interview Pitfalls
- Using a service locator that hides an object’s real dependencies.
- Injecting a short-lived service directly into a singleton.
- Registering mutable non-thread-safe services as singletons.
- Creating interfaces for every class without a useful abstraction boundary.
- Allowing constructors to accumulate many unrelated dependencies.
- Assuming a dependency injection container fixes poor separation of concerns.
What is database normalization, and when might denormalization be appropriate?
Direct Answer
Normalization separates related facts to reduce duplication and update anomalies. Denormalization intentionally duplicates or combines data to simplify or accelerate specific reads.
Detailed Explanation
Database normalization organizes data so that each fact has a clear and consistent place.
Its goals commonly include:
For example, storing a customer’s address in every order row creates duplication. Keeping customers and orders in separate tables allows each order to reference the appropriate customer.
Denormalization intentionally stores duplicated, prejoined, or precomputed information for a specific operational reason. It may be useful when:
Denormalization introduces synchronization risk. The system must define which copy is authoritative and how duplicated values are updated, rebuilt, or reconciled.
Code Example
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE
);
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
customer_id bigint NOT NULL
REFERENCES customers(id),
ordered_at timestamptz NOT NULL DEFAULT now()
);Common Interview Pitfalls
- Treating normalization as a requirement to split every field into another table.
- Denormalizing before measuring an actual query-performance problem.
- Duplicating data without identifying the authoritative source.
- Ignoring the consistency work required when duplicated values change.
How do primary keys, unique constraints, and foreign keys differ?
Direct Answer
A primary key identifies each row, a unique constraint prevents duplicate values, and a foreign key requires referenced values to exist in another compatible key.
Detailed Explanation
These constraints protect different aspects of relational integrity.
Foreign keys establish referential integrity between tables. Their actions must be chosen according to the domain:
RESTRICT or NO ACTION can prevent deletion of referenced rows.CASCADE can propagate deletion or key updates.SET NULL can remove the relationship while retaining the referencing row when null is allowed.Constraint behavior should model business ownership. Cascading deletion is appropriate only when the child record should not exist independently of its parent.
Code Example
CREATE TABLE teams (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name text NOT NULL UNIQUE
);
CREATE TABLE members (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
team_id bigint NOT NULL,
email text NOT NULL UNIQUE,
CONSTRAINT members_team_fk
FOREIGN KEY (team_id)
REFERENCES teams(id)
ON DELETE RESTRICT
);Common Interview Pitfalls
- Using a nullable column as part of required entity identity.
- Assuming a foreign key automatically creates an index on every referencing column.
- Using cascading deletion without considering the ownership relationship.
- Relying only on application code to maintain referential integrity.
- Confusing a unique business key with the table primary key.
How do inner joins and outer joins differ, and how can joins unexpectedly multiply rows?
Direct Answer
An inner join keeps matching rows, while outer joins also preserve unmatched rows from one or both sides. One-to-many matches can multiply result rows.
Detailed Explanation
A join combines rows according to a matching condition.
NULL.Join cardinality depends on the relationship between matching values. If one customer has five orders, joining the customer to orders produces five result rows for that customer.
Unexpected multiplication often occurs when:
Developers should understand the expected relationship—one-to-one, one-to-many, or many-to-many—before writing the query.
Code Example
SELECT
c.id,
c.name,
COUNT(o.id) AS order_count
FROM customers AS c
LEFT JOIN orders AS o
ON o.customer_id = c.id
GROUP BY c.id, c.name
ORDER BY c.id;Common Interview Pitfalls
- Using an inner join when unmatched parent rows must remain visible.
- Forgetting a join condition and accidentally producing a cross join.
- Assuming one output row will always correspond to one left-side row.
- Counting joined rows without considering duplicated matches.
- Filtering nullable outer-joined columns in a way that removes unmatched rows.
What does transaction isolation control, and why can concurrent transactions still conflict?
Direct Answer
Isolation controls which concurrent changes a transaction may observe. Stronger isolation reduces anomalies but may cause blocking, serialization failures, or retries.
Detailed Explanation
A transaction groups database operations into one logical unit that either commits or rolls back.
Isolation defines how concurrent transactions interact and which intermediate or committed changes they can observe.
Common anomalies include:
PostgreSQL exposes READ COMMITTED, REPEATABLE READ, and SERIALIZABLE; requesting READ UNCOMMITTED behaves like READ COMMITTED.
Stronger isolation does not mean that every transaction simply succeeds more safely. The database may block conflicting operations or abort a transaction with a serialization failure. Applications using strong isolation must be prepared to retry the entire transaction safely.
Code Example
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT available_quantity
FROM inventory
WHERE product_id = 42
FOR UPDATE;
UPDATE inventory
SET available_quantity = available_quantity - 1
WHERE product_id = 42
AND available_quantity > 0;
COMMIT;Common Interview Pitfalls
- Assuming a transaction automatically prevents every concurrency anomaly.
- Performing external side effects inside a transaction that may be retried.
- Retrying only the failed statement instead of the entire transaction.
- Keeping transactions open while waiting for user input or network calls.
- Selecting an isolation level without identifying the protected invariant.
How do database indexes improve reads, and why does column order matter in a composite index?
Direct Answer
Indexes provide faster paths to matching rows but add storage and write overhead. In a multicolumn B-tree index, leading-column conditions usually determine efficient scan boundaries.
Detailed Explanation
An index stores values in a structure that can help the database locate rows without scanning the entire table.
Indexes can improve:
They also introduce costs:
For a multicolumn B-tree index such as (account_id, created_at), queries filtering by account_id can usually use the leading portion effectively. Queries using both account_id and created_at may narrow the scan further.
A query filtering only by created_at may not benefit as strongly from that ordering, although planner capabilities and database versions can affect the exact plan.
Index design should begin with actual query patterns, selectivity, ordering requirements, and measured execution plans—not with indexing every column.
Code Example
CREATE INDEX applications_user_created_idx
ON job_applications (user_id, created_at DESC);
SELECT id, company, status, created_at
FROM job_applications
WHERE user_id = 42
ORDER BY created_at DESC
LIMIT 20;Common Interview Pitfalls
- Adding an index to every column without considering write overhead.
- Ignoring the order of columns in a composite B-tree index.
- Keeping duplicate or unused indexes indefinitely.
- Assuming the database must use an index whenever one exists.
- Indexing low-selectivity values without considering the actual query pattern.
How should an engineer use EXPLAIN and EXPLAIN ANALYZE to investigate a slow query?
Direct Answer
EXPLAIN shows the planner’s estimated execution plan, while EXPLAIN ANALYZE executes the query and reports actual timing and row counts for comparison.
Detailed Explanation
EXPLAIN displays the plan selected by the PostgreSQL query planner without executing the statement in its ordinary form.
Important plan information includes:
EXPLAIN ANALYZE executes the statement and adds actual timing, loop counts, and actual rows. Comparing estimated rows with actual rows can reveal inaccurate statistics, skewed data, or predicates whose selectivity is difficult to estimate.
A disciplined investigation should:
1. Capture the exact slow query and representative parameters.
2. Inspect its plan under realistic data volume.
3. Compare estimated and actual rows.
4. Identify the operation consuming the most time or processing excessive rows.
5. Check indexes, join conditions, filtering, sorting, and table statistics.
6. Change one relevant factor and measure again.
Because EXPLAIN ANALYZE runs the statement, using it with data-changing operations can produce real side effects unless it is wrapped and rolled back safely.
Code Example
EXPLAIN (
ANALYZE,
BUFFERS,
VERBOSE
)
SELECT
a.id,
a.company,
a.created_at
FROM job_applications AS a
WHERE a.user_id = 42
AND a.status = 'applied'
ORDER BY a.created_at DESC
LIMIT 20;Common Interview Pitfalls
- Looking only for whether an index appears in the plan.
- Ignoring large differences between estimated and actual row counts.
- Testing with unrepresentative data or parameters.
- Running EXPLAIN ANALYZE on a modifying statement without controlling side effects.
- Optimizing one query without measuring the overall workload impact.
- Assuming the operation with the highest displayed cost is always the root cause.
How should REST API resources and HTTP methods be designed?
Direct Answer
REST APIs model domain resources with stable URLs and use HTTP methods according to their defined semantics, such as GET for retrieval, POST for processing, PUT for replacement, and DELETE for removal.
Detailed Explanation
A resource-oriented API exposes domain concepts through identifiers such as /users, /orders, or /orders/{orderId}. Resource paths usually use nouns because the HTTP method communicates the requested action.
Common HTTP method semantics include:
The API should not map every internal database table or service method directly to a public endpoint. Resources should reflect concepts that clients understand and should hide unnecessary implementation details.
Method semantics matter to clients, gateways, caches, monitoring systems, and retry mechanisms. A GET endpoint should not perform an unexpected business mutation merely because implementing it that way is convenient.
Code Example
GET /api/projects
GET /api/projects/42
POST /api/projects
PUT /api/projects/42
PATCH /api/projects/42
DELETE /api/projects/42Common Interview Pitfalls
- Using action-heavy paths such as getProject or deleteProject instead of HTTP method semantics.
- Performing state-changing operations through GET requests.
- Exposing internal database structure directly as the public API contract.
- Using PUT for partial updates without defining replacement semantics.
- Creating inconsistent pluralization and path conventions across resources.
How should an API choose HTTP status codes and structure error responses?
Direct Answer
An API should select the status code that describes the HTTP outcome and return a consistent machine-readable error body, such as RFC 9457 Problem Details, with safe diagnostic information.
Detailed Explanation
HTTP status codes communicate the general outcome of a request independently of the application-specific response body.
Common examples include:
Location header.RFC 9457 defines Problem Details for consistent machine-readable errors. Standard members include type, title, status, detail, and instance. APIs may add extension fields, such as validation errors or a correlation identifier.
Error responses should not expose stack traces, SQL statements, secrets, internal file paths, or sensitive account information.
Code Example
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"detail": "One or more fields are invalid.",
"instance": "/api/applications/request-729",
"errors": {
"email": ["A valid email address is required."]
}
}Common Interview Pitfalls
- Returning HTTP 200 for every response and placing failure status only in JSON.
- Using 401 and 403 interchangeably.
- Returning different error structures from different endpoints.
- Exposing stack traces or sensitive implementation details to clients.
- Using 500 for validation errors or expected business conflicts.
What is API idempotency, and how can an API make retries safe?
Direct Answer
An operation is idempotent when repeated identical requests have the same intended server effect as one request. Non-idempotent operations can use scoped idempotency keys to deduplicate retries.
Detailed Explanation
Idempotency allows clients to retry requests after timeouts or connection failures without unintentionally repeating the business operation.
HTTP defines safe methods and methods such as PUT and DELETE as idempotent by semantics. This concerns the intended server effect, not whether every response is identical.
For example, deleting the same resource twice may return different status codes, but the intended final state remains that the resource is absent.
A POST operation is not inherently idempotent. A create or payment endpoint can support safe retries through an idempotency key:
1. The client creates one unique key for one logical operation.
2. The server scopes the key to the authenticated caller and endpoint.
3. The server stores the key, normalized request identity, status, and result.
4. A retry with the same key and equivalent payload returns the stored result.
5. Reusing the key with a different payload is rejected.
The idempotency record and business transaction must be coordinated atomically or through an equivalent durable design. Otherwise, the operation could succeed while recording the key fails, allowing a retry to create a duplicate.
Code Example
POST /api/orders
Idempotency-Key: 814629e3-5450-48fb-b7c3-ab81188b3c46
Content-Type: application/json
{
"customerId": "customer-42",
"items": [
{
"productId": "product-9",
"quantity": 2
}
]
}Common Interview Pitfalls
- Assuming every POST request is automatically safe to retry.
- Reusing one idempotency key for different logical operations.
- Ignoring payload differences when the same key is submitted again.
- Recording the idempotency key separately from the business transaction without coordination.
- Keeping idempotency keys globally scoped instead of associating them with the caller.
When should an API be versioned, and how can backward compatibility be preserved?
Direct Answer
Version an API when an unavoidable breaking contract change is introduced. Preserve compatibility through additive changes, tolerant clients, deprecation periods, migration guidance, and contract testing.
Detailed Explanation
An API version represents a contract that clients depend on. A new version may be necessary when a change cannot be introduced without breaking existing clients.
Potentially breaking changes include:
Many changes can remain backward-compatible when they are additive. Examples include adding an optional response field, adding a new endpoint, or supporting an additional optional request parameter.
Version identifiers may appear in the path, query string, header, or media type. The chosen strategy should be consistent and clearly documented.
A responsible version lifecycle includes:
Versioning should not replace careful contract design. Creating a new API version for every internal implementation change unnecessarily fragments clients and increases maintenance cost.
Code Example
GET /api/v1/customers/42
Accept: application/json
GET /api/v2/customers/42
Accept: application/jsonCommon Interview Pitfalls
- Creating a new public API version for every internal code change.
- Removing fields without a deprecation and migration period.
- Assuming adding a required request field is backward-compatible.
- Changing the meaning of an existing field without changing the contract.
- Maintaining old versions indefinitely without usage monitoring or retirement criteria.
What is the difference between authentication and authorization in an API?
Direct Answer
Authentication establishes the caller’s identity, while authorization decides whether that identity may perform a specific action on a particular resource.
Detailed Explanation
Authentication verifies who or what is making a request. It may use a session, access token, client certificate, API credential, or another identity mechanism.
Authorization evaluates whether the authenticated identity is permitted to perform the requested operation.
Authorization can include several levels:
Authentication does not prove ownership. An authenticated user who changes /users/42/documents/8 to /users/42/documents/9 must not gain access merely because the identifier exists.
Authorization must be enforced server-side for every relevant request. Filtering records in the user interface or hiding a button is not an access-control boundary.
APIs should also use HTTPS so credentials and tokens are protected in transit, and they should avoid placing sensitive credentials in URLs.
Code Example
async function getApplication(
actor: AuthenticatedUser,
applicationId: string
): Promise<JobApplication> {
const application =
await applicationRepository.findById(applicationId);
if (
application === null ||
application.userId !== actor.userId
) {
throw new NotFoundError();
}
return application;
}Common Interview Pitfalls
- Treating authentication as proof that a caller can access every resource.
- Checking roles without checking ownership of the requested object.
- Trusting client-supplied user IDs or tenant IDs without server validation.
- Enforcing authorization only by hiding user-interface controls.
- Putting access tokens, passwords, or API keys in request URLs.
- Returning excessive resource details before authorization succeeds.
How should a high-volume API design pagination, filtering, sorting, and rate limiting?
Direct Answer
High-volume APIs should bound page sizes, use stable ordering and cursor pagination where appropriate, validate filters, expose continuation metadata, and communicate enforceable rate limits and retries.
Detailed Explanation
Collection endpoints should avoid returning an unbounded number of records. Pagination protects the service, database, network, and client from excessive work.
Two common pagination approaches are:
offset and limit. It is simple and supports arbitrary page navigation, but large offsets can be expensive and concurrent inserts or deletes can cause records to shift between pages.Reliable pagination requires a deterministic order. Sorting only by a non-unique timestamp can produce ambiguous boundaries, so the API may use a tie-breaker such as (createdAt, id).
Filtering and sorting fields should be explicitly supported and validated. Allowing clients to pass unrestricted database expressions can introduce security, performance, and contract problems.
Rate limiting controls how much traffic a caller may send within defined limits. When rejecting excess traffic, an API commonly returns 429 Too Many Requests and can provide information indicating when a retry may be appropriate.
Clients should use bounded retries with backoff and jitter. Servers should ensure that retries do not amplify load during an outage.
Code Example
GET /api/jobs?status=applied&limit=25&cursor=eyJjcmVhdGVkQXQiOiIyMDI2LTA4LTA0VDE4OjAwOjAwWiIsImlkIjoiam9iLTQyIn0
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
{
"items": [],
"nextCursor": "eyJjcmVhdGVkQXQiOiIyMDI2LTA4LTA0VDE3OjMwOjAwWiIsImlkIjoiam9iLTY3In0",
"hasMore": true
}Common Interview Pitfalls
- Returning an unbounded collection from a public endpoint.
- Using cursor pagination without a deterministic and unique ordering.
- Accepting arbitrary filter or sort expressions from clients.
- Allowing clients to request extremely large page sizes.
- Retrying rate-limited requests immediately without backoff.
- Exposing cursor internals that clients are expected to construct or modify.
How do meaningful names and focused functions improve code quality?
Direct Answer
Meaningful names communicate intent, while focused functions keep related behavior together and reduce the amount of context required to understand, test, and change code.
Detailed Explanation
Names and function boundaries are two of the primary ways code communicates its design.
A meaningful name should explain:
For example, retryDelaySeconds communicates more than delay, while findActiveSubscription communicates more than getData.
A focused function performs one coherent responsibility at one level of abstraction. This does not require an arbitrary maximum number of lines. A function may contain several statements when they collectively implement one understandable operation.
Focused functions are generally easier to:
A function that validates input, performs database access, applies business rules, sends email, and formats an HTTP response likely contains several responsibilities and change reasons.
Code Example
// Too vague and overloaded
void process(Order order) {
validate(order);
save(order);
emailCustomer(order);
}
// Clearer orchestration through named operations
void placeOrder(Order order) {
validateOrder(order);
orderRepository.save(order);
confirmationService.sendFor(order);
}Common Interview Pitfalls
- Using generic names such as data, value, result, helper, or manager without useful context.
- Splitting every statement into a separate function regardless of readability.
- Giving a broad function a narrow name that hides additional side effects.
- Encoding obsolete implementation details into public method names.
- Choosing short names that require readers to inspect the implementation repeatedly.
When should code use comments or documentation instead of relying only on self-explanatory implementation?
Direct Answer
Code should express what it does through structure and naming, while comments and API documentation should explain contracts, intent, constraints, tradeoffs, and non-obvious decisions.
Detailed Explanation
Readable code and documentation serve related but different purposes.
Code should normally communicate the immediate mechanics through:
Comments and documentation are valuable when they explain information that cannot be expressed clearly by implementation alone, including:
A comment that merely restates the code can become noise. A comment that contradicts the implementation is worse because readers may trust outdated information.
When code changes, nearby comments and public documentation should be reviewed as part of the same change. Public APIs generally require stronger documentation than private implementation details because callers cannot safely depend on hidden knowledge.
Code Example
/**
* Reserves inventory for one order.
*
* <p>The caller may safely retry with the same reservation ID.
* A different payload using the same ID is rejected.
*
* @throws InsufficientInventoryException
* when the requested quantity is unavailable
*/
Reservation reserve(
ReservationId reservationId,
ProductId productId,
int quantity
) {
// The database constraint is the final protection
// against concurrent over-reservation.
return reservationRepository.reserve(
reservationId,
productId,
quantity
);
}Common Interview Pitfalls
- Writing comments that repeat the implementation without adding intent or context.
- Using comments to compensate for misleading names or unnecessarily complex code.
- Leaving documentation unchanged after the behavior or contract changes.
- Documenting implementation details as if they were stable public guarantees.
- Adding TODO comments without ownership, context, or a trackable follow-up.
What is a code smell, and how should a developer decide whether to refactor it?
Direct Answer
A code smell is a warning that a design may be difficult to understand or change. It should be evaluated using context, change frequency, defect risk, and maintenance cost.
Detailed Explanation
A code smell is an indicator of a possible design or maintainability problem. It is not proof that the code is incorrect.
Common examples include:
The importance of a smell depends on context. A duplicated three-line calculation in stable code may be less urgent than a duplicated authorization rule that changes frequently and creates security risk.
Before refactoring, consider:
The goal is not to eliminate every smell. The goal is to improve the cost and safety of future changes where the benefit justifies the work.
Common Interview Pitfalls
- Treating every code smell as a confirmed defect.
- Applying a design pattern before understanding the actual change pressure.
- Refactoring stable code only to satisfy a personal style preference.
- Ignoring a smell that repeatedly causes defects or slows delivery.
- Evaluating maintainability without considering the surrounding system and team.
How can Extract Method reduce complexity and duplication without creating unnecessary abstraction?
Direct Answer
Extract Method moves a coherent block into a well-named function, improving readability or reuse. It should represent a meaningful concept rather than merely reducing line count.
Detailed Explanation
Extract Method, also called Extract Function, moves a selected block of code into a new function and replaces the original block with a call to that function.
It can help when:
The quality of the extraction depends heavily on the new boundary. An extracted function that takes ten unrelated parameters and modifies several external variables may reveal that the data or responsibility belongs elsewhere.
Duplication should be assessed by meaning, not only by textual similarity. Two code blocks may look similar today but represent different business rules that will evolve independently. Combining them prematurely can create condition-heavy abstractions that are harder to change than the original duplication.
A good extraction should usually make the caller read closer to the language of the problem domain.
Code Example
Money calculateInvoiceTotal(Invoice invoice) {
Money subtotal = calculateSubtotal(invoice.lines());
Money tax = calculateTax(subtotal, invoice.taxRegion());
Money discount = calculateDiscount(
subtotal,
invoice.customerTier()
);
return subtotal.add(tax).subtract(discount);
}
private Money calculateSubtotal(
List<InvoiceLine> lines
) {
return lines.stream()
.map(InvoiceLine::total)
.reduce(Money.zero(), Money::add);
}Common Interview Pitfalls
- Extracting functions only to satisfy an arbitrary line-count limit.
- Creating vague helper methods that hide rather than clarify behavior.
- Combining coincidentally similar code that represents different domain rules.
- Passing many unrelated parameters into every extracted function.
- Moving duplication into a shared utility that becomes a dependency for unrelated modules.
How should a developer use automated tests to refactor code safely?
Direct Answer
Establish tests for observable behavior, make small structural changes, run the relevant suite after each step, and separate behavior changes from refactoring whenever practical.
Detailed Explanation
Refactoring changes internal structure without intentionally changing externally observable behavior. Automated tests provide rapid feedback when a structural change accidentally alters that behavior.
A safe workflow is:
1. Identify the behavior that must remain unchanged.
2. Add or improve tests when important behavior is not protected.
3. Ensure the tests pass before refactoring.
4. Make one small structural change.
5. Run the relevant tests and static checks.
6. Commit or checkpoint the verified change.
7. Repeat until the desired design is reached.
Tests should focus primarily on behavior and contracts rather than incidental implementation details. A test that verifies every private method call may fail during a harmless refactor even though the public result remains correct.
Legacy code may be difficult to test because responsibilities and dependencies are tightly coupled. In that case, characterization tests can capture current behavior before deeper restructuring begins.
Tests reduce risk but do not prove that every behavior is preserved. Code review, static analysis, integration checks, and production observability remain important.
Common Interview Pitfalls
- Beginning a large refactor while the existing test suite is already failing.
- Changing behavior and structure together without making the distinction clear.
- Writing tests that are tightly coupled to private implementation details.
- Making many structural changes before running any verification.
- Assuming passing unit tests prove that integrations and deployments remain correct.
How should a developer balance abstraction, simplicity, and avoiding premature generalization?
Direct Answer
Introduce abstraction when stable common behavior and real variation are understood. Prefer the simplest design that meets current needs while preserving practical paths for future change.
Detailed Explanation
Abstraction can reduce duplication and isolate variation, but every abstraction also introduces concepts, dependencies, and maintenance obligations.
A useful abstraction usually has:
Premature generalization occurs when code is designed around hypothetical future requirements that are not yet understood. It often produces:
Avoiding premature abstraction does not mean ignoring architecture. Security boundaries, data ownership, public contracts, and costly migration risks may require deliberate design before multiple implementations exist.
A practical approach is to implement the current requirement clearly, observe genuine duplication or variation, and refactor once the shared concept can be named accurately. The goal is reversible, understandable design—not either maximum generality or minimum code.
Common Interview Pitfalls
- Creating a generic framework before the first concrete use case is understood.
- Assuming every duplicated line must immediately share one abstraction.
- Using boolean parameters to combine operations with different responsibilities.
- Rejecting all upfront design even when public contracts are expensive to change.
- Preserving an abstraction after its original assumptions are no longer valid.
- Measuring simplicity only by the number of source-code lines.
How should a developer reproduce and isolate a software defect?
Direct Answer
Reproduce the failure consistently, record the environment and inputs, reduce the scenario to its smallest failing case, and change one suspected factor at a time.
Detailed Explanation
Effective debugging begins by converting an unclear report into a repeatable observation.
A useful reproduction process includes:
Isolation means narrowing the problem boundary. A developer can disable optional components, replace dependencies with controlled substitutes, test individual layers, or compare a working environment with a failing one.
Only one relevant variable should be changed at a time whenever practical. Changing several things simultaneously may make the failure disappear without revealing which change mattered.
A minimal reproduction is valuable because it separates the defect from unrelated application complexity and can later become an automated regression test.
Code Example
@Test
void rejectsDuplicateRegistrationEmail() {
UserRepository repository =
new InMemoryUserRepository();
repository.save(
new User("existing@example.com")
);
RegistrationService service =
new RegistrationService(repository);
assertThrows(
DuplicateEmailException.class,
() -> service.register(
"existing@example.com"
)
);
}Common Interview Pitfalls
- Attempting fixes before confirming the failure can be reproduced.
- Changing several variables at once and losing causal information.
- Testing with different data or configuration than the failing environment.
- Keeping unnecessary application components in the reproduction case.
- Ignoring whether the defect began after a specific deployment or dependency update.
How should a developer read a stack trace and correlate it with application logs?
Direct Answer
Start with the exception type and message, inspect the first relevant application frame, follow chained causes, and correlate timestamps, request IDs, and surrounding log events.
Detailed Explanation
A stack trace records the active call sequence when an exception or diagnostic snapshot is produced.
A practical reading process is:
1. Identify the exception type and message.
2. Locate the first stack frame belonging to the application rather than the framework or runtime.
3. Inspect the source file and line number at that frame.
4. Follow each Caused by section to find the underlying exception chain.
5. Review suppressed exceptions when resource-management failures may be relevant.
6. Correlate the event with logs from the same request, job, user operation, or trace.
The top-level exception is not always the root cause. A controller may wrap a database exception, which may itself have been caused by a network or constraint failure.
Useful logs provide structured context such as:
Logs should not expose passwords, access tokens, session secrets, or unnecessary personal data. A useful debugging message describes the failed operation and context without leaking sensitive values.
Code Example
try {
paymentGateway.charge(request);
} catch (GatewayTimeoutException cause) {
logger.error(
"Payment gateway timed out orderId={} requestId={}",
request.orderId(),
requestId,
cause
);
throw new PaymentProcessingException(
"Unable to complete payment",
cause
);
}Common Interview Pitfalls
- Reading only the final exception message and ignoring the chained cause.
- Assuming the first framework frame identifies the application defect.
- Logging an exception without the operation or correlation context.
- Removing the original cause when wrapping an exception.
- Logging credentials, access tokens, or sensitive request contents.
- Searching logs without aligning timestamps and time zones.
How should breakpoints and runtime-state inspection be used effectively?
Direct Answer
Use targeted breakpoints to pause near the suspected transition, inspect variables and call stacks, evaluate assumptions, and avoid changing runtime state in ways that hide the defect.
Detailed Explanation
A debugger allows a developer to pause execution and inspect the program at a specific point.
Useful debugger capabilities include:
Start near the boundary where correct state becomes incorrect rather than stepping through the entire application from startup.
Conditional breakpoints are particularly useful for defects that occur only for one record, iteration, or request. For example, execution can pause only when orderId.equals("order-42").
A debugger can alter timing, especially in concurrent programs. Pausing one thread may prevent or change a race condition. Runtime state modification and expression evaluation may also invoke methods with side effects, so observations should be interpreted carefully.
Code Example
public Money calculateTotal(Order order) {
Money subtotal = calculateSubtotal(order);
// Conditional breakpoint example:
// order.id().equals("order-42")
Money discount =
discountPolicy.apply(order, subtotal);
return subtotal.subtract(discount);
}Common Interview Pitfalls
- Stepping through the entire application without first narrowing the suspected area.
- Using unconditional breakpoints inside high-frequency loops or request paths.
- Changing variable values in the debugger and then trusting the resulting behavior.
- Evaluating methods with side effects while inspecting runtime state.
- Assuming debugger-paused concurrency behaves exactly like normal execution.
What is the difference between fixing a symptom and correcting a root cause?
Direct Answer
A symptom fix reduces the visible failure, while a root-cause correction addresses the underlying condition that created it and prevents recurrence through systemic improvements.
Detailed Explanation
A symptom is an observable consequence of a deeper problem. A service restart may clear an exhausted resource temporarily, but it does not explain why the resource was exhausted.
Root-cause analysis investigates the causal chain behind the incident. A useful process includes:
1. Define the observed failure and its impact.
2. Build a timeline using deployments, logs, metrics, traces, and configuration changes.
3. Separate confirmed evidence from assumptions.
4. Identify the earliest incorrect condition that explains the later symptoms.
5. Test the causal hypothesis against the evidence.
6. Correct the immediate defect.
7. Add prevention, detection, and recovery improvements.
A root cause is not necessarily one person’s mistake or one line of code. Incidents often involve several contributing conditions, such as an unsafe default, missing validation, weak monitoring, and an incomplete rollout process.
Techniques such as the Five Whys can help explore causal relationships, but they should not force every incident into one simplistic cause.
A complete corrective action may include code changes, tests, alerts, documentation, deployment safeguards, and operational playbooks.
Common Interview Pitfalls
- Treating a restart or retry as proof that the underlying problem is solved.
- Stopping the investigation at the first human mistake found.
- Writing a root cause that is not supported by incident evidence.
- Using the Five Whys mechanically to force one simple explanation.
- Correcting code without improving detection or preventing recurrence.
- Blaming an individual instead of examining system and process conditions.
How should a developer diagnose CPU, memory, thread, or latency problems?
Direct Answer
Measure the affected resource, capture evidence during the failure, identify where time or memory is consumed, form a hypothesis, and verify improvements with comparable measurements.
Detailed Explanation
Performance debugging should begin with measurement rather than intuition.
First determine which symptom is present:
Useful JVM evidence includes:
Oracle recommends jcmd as a primary diagnostic utility for running JVMs. It can print threads, create heap dumps and class histograms, and control Java Flight Recorder sessions.
Performance should be measured across distributions rather than only averages. Averages can hide severe tail latency, so percentiles such as p95 or p99 may better represent user impact.
After identifying a likely bottleneck, change one relevant factor and compare results under similar load and data conditions. An optimization that improves one endpoint may increase memory usage, write cost, or downstream load elsewhere.
Code Example
# Print Java threads
jcmd <pid> Thread.print
# Create a class histogram
jcmd <pid> GC.class_histogram
# Start a two-minute Java Flight Recorder capture
jcmd <pid> JFR.start \
name=diagnostic \
settings=profile \
duration=2m \
filename=diagnostic.jfrCommon Interview Pitfalls
- Optimizing code before measuring where time or resources are consumed.
- Looking only at average latency and ignoring tail latency.
- Taking a heap dump after restarting and losing the failing state.
- Assuming high memory usage always proves a memory leak.
- Collecting one thread dump instead of comparing several samples.
- Improving one component without checking downstream or system-wide effects.
How should an engineer investigate an intermittent failure that occurs only in production?
Direct Answer
Preserve evidence, correlate logs, metrics, traces, deployments, and affected requests, compare successful and failed cases, and test hypotheses without destabilizing production.
Detailed Explanation
Intermittent production failures are difficult because the triggering combination of timing, data, load, dependency state, or infrastructure conditions may be rare.
A disciplined investigation should:
1. Define the failure signature and user impact.
2. Capture timestamps, request IDs, trace IDs, affected versions, regions, instances, and input characteristics.
3. Compare failed requests with successful requests from the same period.
4. Correlate deployments, configuration changes, dependency errors, resource saturation, and traffic patterns.
5. Determine whether failures cluster by instance, tenant, data shape, region, browser, or concurrency level.
6. Collect thread dumps, profiles, or diagnostic recordings when safe.
7. Form a falsifiable hypothesis and identify evidence that would confirm or reject it.
8. Reproduce the relevant condition in staging, load tests, or a controlled canary when possible.
Distributed tracing helps follow one request across services and identify where latency or errors first appear. Structured logs and consistent identifiers make cross-service correlation possible.
Mitigation and diagnosis should be separated. Rolling back or disabling a feature may protect users before the root cause is known. Preserve relevant evidence before restarting or replacing affected instances when operationally safe.
Production debugging must respect privacy, security, performance, and availability. Debug logging should be targeted and temporary rather than exposing sensitive information or generating uncontrolled volume.
Common Interview Pitfalls
- Restarting affected instances before preserving useful diagnostic evidence.
- Enabling unrestricted debug logging across all production traffic.
- Assuming correlation between a deployment and failure proves causation.
- Investigating only failed requests without comparing successful cases.
- Changing several production controls at once and losing causal clarity.
- Ignoring rare data shapes, retries, timeouts, and concurrency conditions.
Official Documentation & Specifications
Object-Oriented Design
- Oracle Object-Oriented Objects
- Oracle Member Access Control
- Oracle Inheritance Documentation
- Oracle Interface Documentation
- Oracle Abstract Methods and Classes
- Oracle Sealed Classes and Interfaces
- Oracle Polymorphism Documentation
- Oracle Overriding and Hiding
- Java Language Specification Classes
- Java Object Equality Contract
- Java Objects Utility Documentation
- OpenJDK Record Classes
- Microsoft Dependency Injection Overview
- Microsoft Dependency Injection Guidelines
- Microsoft Service Lifetime Documentation
- Microsoft Architectural Principles
Relational Databases
- PostgreSQL Data Definition
- PostgreSQL Constraints
- PostgreSQL Foreign Keys
- PostgreSQL Unique Indexes
- PostgreSQL Join Tutorial
- PostgreSQL Table Expressions
- PostgreSQL Transactions Tutorial
- PostgreSQL Transaction Isolation
- PostgreSQL Explicit Locking
- PostgreSQL Indexes
- PostgreSQL Multicolumn Indexes
- PostgreSQL Index Ordering
- PostgreSQL Partial Indexes
- PostgreSQL Using EXPLAIN
- PostgreSQL EXPLAIN Command
- PostgreSQL ANALYZE Command
API Development & REST
Clean Code & Refactoring
Debugging & Troubleshooting
- Oracle Preparing for Troubleshooting
- GitHub Issue Documentation
- Java Throwable Documentation
- Oracle Chained Exceptions
- OWASP Logging Guidance
- Java Debugger Documentation
- Java Platform Debugger Architecture
- AWS Root Cause Analysis
- AWS Five Whys Analysis
- AWS Incident Investigation Playbooks
- Oracle Java Diagnostic Tools
- Oracle jcmd Documentation
- Oracle Java Flight Recorder Tool
- AWS Observability Guidance
- AWS Workload Trace Analysis
- AWS Distributed Tracing Guidance
- GitHub Targeted Debug Logging
- Kubernetes Application Troubleshooting
Want to tailer your resume for Software Developer roles?
Import your resume, scan it for critical Software Developer keywords, and compare it against ATS standards instantly.