Backend Developer Interview Questions
Core Overview
Prepare for backend developer interviews covering server-side fundamentals, API integration, data modeling, scalable systems, security, reliability, and observability.
Ready to test your knowledge?
Launch a focused practice session to review questions without distraction.
What happens in a backend application from the time an HTTP request arrives until a response is returned?
Direct Answer
The server accepts and parses the request, routes it to application logic, authenticates and validates it, performs required work, and serializes an HTTP response.
Detailed Explanation
A typical backend request passes through several stages:
1. A web server, reverse proxy, or application runtime accepts the network connection.
2. The HTTP request is parsed into a method, target URI, headers, and optional content.
3. Routing selects the matching endpoint or controller.
4. Middleware may perform authentication, authorization, logging, tracing, rate limiting, or request transformation.
5. The endpoint validates the request and invokes application or domain logic.
6. The application may communicate with databases, caches, message brokers, or external services.
7. The result is mapped to an HTTP status code, headers, and response representation.
8. Observability data such as duration, status, and trace context is recorded.
These stages should have clear responsibilities. A controller should generally coordinate the request rather than contain database queries, complex business rules, and infrastructure-specific code directly.
The request may also fail at any stage. A consistent exception-mapping layer should translate expected failures into appropriate client responses while preventing internal details from leaking.
Code Example
@RestController
@RequestMapping("/applications")
final class ApplicationController {
private final CreateApplicationUseCase createApplication;
ApplicationController(
CreateApplicationUseCase createApplication
) {
this.createApplication = createApplication;
}
@PostMapping
ResponseEntity<ApplicationResponse> create(
@Valid @RequestBody CreateApplicationRequest request
) {
Application result =
createApplication.execute(request.toCommand());
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApplicationResponse.from(result));
}
}Common Interview Pitfalls
- Putting routing, validation, business logic, and database access into one controller method.
- Returning internal exception details directly to the client.
- Assuming every request reaches the controller before it can fail.
- Ignoring correlation IDs and timing information during request processing.
- Performing expensive work before authentication and basic validation.
What is the difference between a stateless and a stateful backend service?
Direct Answer
A stateless service does not depend on instance-local data from earlier requests, while a stateful service retains session or workflow state that affects later interactions.
Detailed Explanation
A stateless backend instance can process a request without depending on request-specific data retained locally from a previous interaction.
This does not mean the overall system contains no state. User accounts, sessions, orders, jobs, and other durable information still exist, but they are stored in shared systems such as:
Stateless application instances are easier to replace and scale horizontally because any healthy instance can process the next request.
A stateful service retains information locally that affects future requests or connections. Examples include an in-memory game session, a long-lived WebSocket connection, or a workflow engine holding active execution state.
Stateful designs are sometimes necessary, but they require deliberate handling of routing, replication, recovery, failover, and data ownership.
A common mistake is storing authenticated session data only in one server’s memory and then adding several instances behind a load balancer. A later request routed to another instance may not find the session unless sticky routing or shared session storage is used.
Code Example
// Application instances remain stateless.
// Session data is stored in a shared repository.
final class SessionService {
private final SessionRepository sessions;
SessionService(SessionRepository sessions) {
this.sessions = sessions;
}
Optional<UserSession> find(String sessionId) {
return sessions.findById(sessionId);
}
}Common Interview Pitfalls
- Assuming stateless means that the application does not store any data.
- Keeping user sessions only in one application instance without accounting for failover.
- Using sticky sessions as the only protection against instance failure.
- Storing mutable shared state in singleton objects without concurrency protection.
- Moving state externally without considering consistency and availability requirements.
When should a backend operation be processed synchronously versus asynchronously?
Direct Answer
Use synchronous processing when the client needs an immediate result and work is bounded. Use asynchronous processing for long-running, bursty, or independently retryable work.
Detailed Explanation
In synchronous processing, the request remains active while the backend completes the required work and returns the result.
This is appropriate when:
In asynchronous processing, the backend accepts the request, records or queues the work, and returns before processing is complete. The client may receive 202 Accepted and a status-resource location.
This is useful for:
Asynchronous processing introduces additional responsibilities: durable job storage, idempotency, retry policies, status tracking, dead-letter handling, observability, and eventual completion semantics.
Asynchronous should not be confused with nonblocking programming. A server may use nonblocking I/O while still completing the client’s logical request synchronously.
Code Example
POST /api/reports
Content-Type: application/json
{
"type": "application-history",
"dateRange": "last-12-months"
}
HTTP/1.1 202 Accepted
Location: /api/report-jobs/job-42
{
"jobId": "job-42",
"status": "queued"
}Common Interview Pitfalls
- Holding an HTTP request open for unbounded background work.
- Returning success before ensuring asynchronous work was stored durably.
- Retrying asynchronous work without making the operation idempotent.
- Creating background jobs without status tracking or failure visibility.
- Confusing nonblocking I/O with asynchronous business completion.
Why do backend servers use thread pools, and what problems can poorly configured pools cause?
Direct Answer
Thread pools reuse a bounded set of workers and control concurrency. Poor sizing or unbounded queues can cause latency, memory growth, starvation, or overload propagation.
Detailed Explanation
Creating a new platform thread for every unit of work can introduce scheduling, memory, and lifecycle overhead. A thread pool separates task submission from worker management and can place limits on concurrent execution.
Important configuration concepts include:
A CPU-bound workload is constrained primarily by available processors. Adding far more active threads than cores can increase context switching without increasing useful throughput.
An I/O-bound workload may tolerate more concurrency because workers often wait for databases or networks. However, increasing thread count cannot compensate indefinitely for a slow downstream dependency.
An unbounded work queue can hide overload temporarily while latency and memory consumption continue to grow. A bounded queue with an explicit rejection or backpressure policy makes capacity limits visible.
Separate pools may be appropriate when one slow workload must not consume every worker needed by critical requests.
Code Example
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
8,
16,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.AbortPolicy()
);
try {
executor.execute(task);
} catch (RejectedExecutionException rejected) {
metrics.increment("tasks.rejected");
throw new ServiceOverloadedException();
}Common Interview Pitfalls
- Using an unbounded queue and assuming queued work has no cost.
- Increasing thread count without measuring downstream capacity.
- Running blocking tasks in a pool intended for CPU-intensive work.
- Ignoring rejected tasks instead of defining overload behavior.
- Allowing one slow workload to consume every shared worker.
- Sizing a pool using a fixed rule without measuring the actual workload.
How should controllers, application services, domain logic, and repositories be separated in a backend?
Direct Answer
Controllers handle transport concerns, application services coordinate use cases, domain code enforces business rules, and repositories abstract persistence operations.
Detailed Explanation
Layering helps separate reasons for change and prevents transport or database details from spreading throughout business logic.
A practical separation is:
The exact number of layers should fit the application. A small CRUD service does not need ceremonial abstractions for every operation, while complex business rules benefit from remaining independent of controllers and ORM entities.
Dependencies should generally point toward stable business policies. Domain logic should not need to know which HTTP framework, database driver, or cloud provider is being used.
A repository is not merely a wrapper around every SQL statement. Its interface should represent persistence needs meaningful to the use case.
Code Example
interface JobApplicationRepository {
Optional<JobApplication> findById(
JobApplicationId id
);
void save(JobApplication application);
}
final class RejectApplicationUseCase {
private final JobApplicationRepository repository;
RejectApplicationUseCase(
JobApplicationRepository repository
) {
this.repository = repository;
}
void execute(JobApplicationId id) {
JobApplication application = repository
.findById(id)
.orElseThrow(ApplicationNotFound::new);
application.reject();
repository.save(application);
}
}Common Interview Pitfalls
- Placing business rules directly in controllers or database callbacks.
- Creating pass-through layers that add names but no meaningful responsibility.
- Allowing domain logic to depend directly on a web framework.
- Exposing ORM entities as the permanent public API contract.
- Creating one generic repository interface that cannot express domain-specific queries.
- Adding complex architecture to a simple service without clear benefit.
How should a team decide between a monolith, modular monolith, and independently deployed services?
Direct Answer
Choose boundaries from domain ownership, scaling, reliability, deployment, and team needs. Independent services add operational complexity and should solve a demonstrated problem.
Detailed Explanation
A monolith packages and deploys the application as one unit. It can be straightforward to develop, test, deploy, and operate when the product and team are still small.
A modular monolith remains one deployable unit but enforces internal module boundaries, ownership, and dependency rules. This can preserve operational simplicity while preventing the codebase from becoming one tightly coupled structure.
Independently deployed services can provide:
They also introduce:
A service boundary should represent cohesive business capability or ownership, not simply one database table or source-code class.
Teams should avoid decomposing solely because microservices appear more modern. A modular monolith is often a strong starting point when domain boundaries are still evolving. Extraction becomes easier once actual scaling, ownership, deployment, or reliability pressure is visible.
Common Interview Pitfalls
- Creating one service for every database table or entity.
- Choosing microservices before establishing deployment and observability capabilities.
- Sharing one mutable database schema across supposedly independent services.
- Assuming network calls behave like reliable in-process method calls.
- Splitting services before domain ownership and boundaries are understood.
- Treating a monolith as inherently unscalable or poorly designed.
How should a backend developer design resource-oriented HTTP API endpoints?
Direct Answer
Model client-facing domain resources with stable URLs, use HTTP methods according to their semantics, and avoid exposing internal service methods or database structures directly.
Detailed Explanation
A resource-oriented API represents concepts that clients understand through URLs such as /users, /orders, and /orders/{orderId}.
The resource path normally identifies what the client is interacting with, while the HTTP method communicates the requested operation:
Paths should generally use nouns rather than controller-style actions such as /getOrder or /deleteUser.
Nested paths can communicate meaningful ownership, for example /projects/{projectId}/tasks, but excessive nesting can create rigid and difficult-to-maintain APIs. When a child resource has its own stable identity, /tasks/{taskId} may be sufficient for direct access.
Public API contracts should not mirror every table, internal class, or backend service method. Internal implementation can change while the external resource model remains stable.
Code Example
GET /api/projects
POST /api/projects
GET /api/projects/42
PATCH /api/projects/42
DELETE /api/projects/42
GET /api/projects/42/tasks
POST /api/projects/42/tasks
GET /api/tasks/91Common Interview Pitfalls
- Using action-based endpoint names for ordinary resource operations.
- Performing state-changing operations through GET requests.
- Exposing database tables directly as the public resource model.
- Creating deeply nested paths that clients cannot use independently.
- Using inconsistent naming and pluralization across related endpoints.
How should a backend API use HTTP status codes and consistent error contracts?
Direct Answer
Use HTTP status codes to describe the request outcome and return a stable machine-readable error body containing safe details, identifiers, and actionable validation information.
Detailed Explanation
The HTTP status code communicates the general outcome of a request independently of the application-specific response content.
Common examples include:
Location header.Error responses should follow one consistent structure across endpoints. RFC 9457 Problem Details provides standard fields such as type, title, status, detail, and instance.
An API may add fields for validation failures, error codes, or correlation identifiers. Internal stack traces, SQL statements, secrets, infrastructure names, and sensitive account information should not be returned publicly.
Code Example
HTTP/1.1 409 Conflict
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/email-conflict",
"title": "Email already registered",
"status": 409,
"detail": "An account already uses this email address.",
"instance": "/api/users/request-91",
"errorCode": "EMAIL_ALREADY_REGISTERED",
"requestId": "request-91"
}Common Interview Pitfalls
- Returning HTTP 200 for failures and storing the real status only in JSON.
- Using 401 and 403 interchangeably.
- Returning a different error-body format from each endpoint.
- Exposing stack traces or internal exception messages to clients.
- Using 500 for expected validation or business-rule failures.
- Returning detailed resource information before authorization succeeds.
How can a backend make create, payment, or integration operations safe to retry?
Direct Answer
Use a caller-scoped idempotency key, persist the operation result atomically, reject mismatched payload reuse, and return the recorded result for equivalent retries.
Detailed Explanation
A client may not know whether an operation succeeded when the connection closes or times out before the response arrives. Retrying without protection can create duplicate orders, charges, emails, or external requests.
HTTP defines methods such as PUT and DELETE as idempotent according to their intended effect. POST is not inherently idempotent, but an API can add retry safety through an idempotency key.
A robust workflow is:
1. The client creates one unique key for one logical operation.
2. The backend scopes the key to the authenticated caller and operation.
3. The backend stores a normalized request fingerprint with the key.
4. The business result and idempotency record are committed atomically or through an equivalent durable mechanism.
5. A retry with the same key and equivalent payload returns the stored result.
6. Reusing the key with different content is rejected.
Concurrent requests using the same key must also be coordinated. A unique database constraint, row lock, transactional insert, or dedicated idempotency service can ensure that only one operation executes.
Idempotency records need an explicit retention policy based on the business operation and the maximum expected retry window.
Code Example
POST /api/payments
Idempotency-Key: 041f76df-d703-4389-b630-51e43f551cb9
Content-Type: application/json
{
"orderId": "order-42",
"amount": 4999,
"currency": "USD"
}Common Interview Pitfalls
- Assuming a POST request is automatically safe to retry.
- Scoping idempotency keys globally instead of by caller and operation.
- Accepting the same key with a different request payload.
- Persisting the idempotency record separately from the business result without coordination.
- Ignoring concurrent requests that submit the same key simultaneously.
- Deleting idempotency records before legitimate client retries have ended.
How should a backend combine timeouts, retries, backoff, and circuit breakers for remote calls?
Direct Answer
Set bounded timeouts, retry only transient and safe failures with backoff and jitter, limit attempts, and use a circuit breaker to fail fast during persistent dependency failures.
Detailed Explanation
Remote calls can fail through timeouts, connection errors, throttling, unavailable dependencies, or application-level failures.
A resilient call strategy contains several distinct controls:
A circuit breaker commonly has three states:
Retries should be finite and should fit within the request’s overall time budget. Retrying at several layers can multiply traffic into a retry storm.
Do not automatically retry permanent failures such as invalid credentials, unsupported requests, or most validation errors. Non-idempotent operations require explicit duplicate protection before retries are safe.
Code Example
RetryPolicy retryPolicy = RetryPolicy.builder()
.maxAttempts(3)
.initialBackoff(Duration.ofMillis(200))
.maxBackoff(Duration.ofSeconds(2))
.jitter(true)
.retryOn(TimeoutException.class)
.retryOn(ServiceUnavailableException.class)
.build();
PaymentResponse response = circuitBreaker.execute(
() -> retryPolicy.execute(
() -> paymentClient.authorize(request)
)
);Common Interview Pitfalls
- Calling remote services without explicit connection and request timeouts.
- Retrying validation, authentication, or other permanent failures.
- Applying retries at several layers and multiplying downstream traffic.
- Retrying non-idempotent operations without duplicate protection.
- Using fixed retry delays across every client without jitter.
- Keeping a circuit permanently open without controlled recovery probes.
How should a backend securely receive and process webhook deliveries?
Direct Answer
Verify the signature over the raw request body, reject invalid deliveries, deduplicate event IDs, persist work durably, acknowledge quickly, and process heavy work asynchronously.
Detailed Explanation
A webhook allows one system to notify another system that an event occurred. Because the receiving endpoint is publicly reachable, the backend must verify that the delivery came from the expected provider and was not modified.
A secure webhook flow is:
1. Read and preserve the raw request body.
2. Obtain the provider’s signature and delivery identifier from headers.
3. Compute the expected signature using the configured secret and documented algorithm.
4. Compare signatures using a timing-safe comparison.
5. Reject missing or invalid signatures before processing.
6. Check whether the delivery identifier was already processed.
7. Persist the event or enqueue durable work.
8. Return a successful response quickly.
9. Process expensive business logic asynchronously.
Webhook providers may retry, redeliver, reorder, or duplicate events. Handlers should therefore be idempotent and should not assume that events arrive exactly once or strictly in order.
Secrets should be stored securely and rotated deliberately. Logging should include delivery identifiers and event types, but not the webhook secret or unnecessary sensitive payload data.
Code Example
public ResponseEntity<Void> receiveWebhook(
byte[] rawBody,
String signature,
String deliveryId
) {
if (!signatureVerifier.isValid(rawBody, signature)) {
return ResponseEntity.status(401).build();
}
boolean accepted = webhookInbox.acceptIfNew(
deliveryId,
rawBody
);
if (accepted) {
webhookQueue.publish(deliveryId);
}
return ResponseEntity.noContent().build();
}Common Interview Pitfalls
- Verifying the signature against parsed or modified JSON instead of the raw body.
- Using ordinary string comparison for security-sensitive signatures.
- Processing duplicate deliveries more than once.
- Performing slow business work before acknowledging the webhook.
- Trusting source IP addresses as the only authentication mechanism.
- Logging webhook secrets or complete sensitive payloads.
- Assuming webhook events always arrive once and in order.
How should a backend isolate and operate a critical third-party API integration?
Direct Answer
Place the provider behind an internal adapter, enforce deadlines and idempotency, normalize contracts and errors, persist reconciliation state, monitor behavior, and design degraded operation.
Detailed Explanation
A third-party integration creates a dependency on an external contract, availability profile, rate limit, security model, and operational process that the backend does not control.
A resilient design places the provider behind an internal interface or adapter. This boundary can:
Critical operations should maintain an internal state machine rather than assuming one request and one response complete the entire workflow. For example, a payment may move through pending, authorized, failed, cancelled, or unknown states.
An unknown outcome must not automatically be treated as failure. The provider may have completed the operation even though the response was lost. Reconciliation through provider lookup APIs, webhooks, or scheduled jobs may be necessary.
The backend should also define degraded behavior. Depending on the feature, it may queue work, return a temporary failure, use cached read data, disable a noncritical capability, or route to another provider.
Operational readiness includes contract tests, sandbox testing, credential rotation, rate-limit monitoring, alerting, provider-status visibility, and a documented incident procedure.
Code Example
interface PaymentProvider {
AuthorizationResult authorize(
PaymentCommand command
);
ProviderPayment findByOperationId(
OperationId operationId
);
}
final class AuthorizePaymentUseCase {
private final PaymentRepository payments;
private final PaymentProvider provider;
Payment authorize(PaymentCommand command) {
Payment payment =
payments.createPendingIfAbsent(command);
try {
AuthorizationResult result =
provider.authorize(command);
payment.record(result);
} catch (UnknownProviderOutcome error) {
payment.markReconciliationRequired();
}
payments.save(payment);
return payment;
}
}Common Interview Pitfalls
- Allowing provider-specific response objects to spread throughout the application.
- Treating a network timeout as proof that the external operation failed.
- Using retries without idempotency or reconciliation support.
- Depending only on synchronous provider responses for final state.
- Ignoring provider rate limits and account-level quotas.
- Failing to store provider request and operation identifiers.
- Designing no degraded behavior for dependency outages.
- Rotating credentials without testing both activation and rollback procedures.
How should a backend developer choose between a relational database and a document database?
Direct Answer
Choose from relationships, transaction needs, query patterns, consistency requirements, schema evolution, and operational experience rather than assuming one database model is universally better.
Detailed Explanation
A relational database organizes information into tables connected through keys and constraints. It is often a strong choice when the system needs:
A document database stores related information in document-shaped records. It can be useful when:
A flexible document schema does not remove the need for modeling. The team still needs to understand access patterns, relationships, document growth, validation, indexing, and migration strategy.
The decision should be based on the most important operations. A system may use a relational database for transactional records and a separate document or search store for specialized read workloads, but every additional database increases operational and consistency complexity.
Common Interview Pitfalls
- Choosing a document database only because the schema may change.
- Assuming relational databases cannot scale horizontally.
- Embedding unbounded collections inside one document.
- Selecting a database before identifying important query and transaction patterns.
- Using several database technologies without a clear workload-specific benefit.
How do primary keys, foreign keys, unique constraints, and check constraints protect data integrity?
Direct Answer
Primary keys identify rows, foreign keys protect relationships, unique constraints prevent duplicate values, and check constraints reject row states that violate defined rules.
Detailed Explanation
Database constraints provide a final integrity boundary even when data is written by several applications, scripts, imports, or background workers.
Common constraints include:
Application validation is still useful because it can provide earlier and more user-friendly feedback. It should not be the only protection for critical invariants that the database can enforce reliably.
Foreign-key actions such as RESTRICT, CASCADE, and SET NULL should reflect ownership semantics. Cascading deletion is appropriate only when the dependent row should not survive independently.
A constraint name should be stable and descriptive so database errors can be mapped to meaningful application errors.
Code Example
CREATE TABLE organizations (
id bigint GENERATED ALWAYS AS IDENTITY,
name text NOT NULL,
PRIMARY KEY (id),
CONSTRAINT organizations_name_unique
UNIQUE (name)
);
CREATE TABLE users (
id bigint GENERATED ALWAYS AS IDENTITY,
organization_id bigint NOT NULL,
email text NOT NULL,
account_status text NOT NULL,
PRIMARY KEY (id),
CONSTRAINT users_organization_fk
FOREIGN KEY (organization_id)
REFERENCES organizations(id)
ON DELETE RESTRICT,
CONSTRAINT users_org_email_unique
UNIQUE (organization_id, email),
CONSTRAINT users_status_check
CHECK (
account_status IN (
'invited',
'active',
'disabled'
)
)
);Common Interview Pitfalls
- Relying only on application validation for critical database invariants.
- Using cascading deletion without understanding record ownership.
- Adding a foreign key without considering lookup and deletion performance.
- Using nullable columns for values required by the domain.
- Creating generic constraint names that are difficult to diagnose.
- Assuming a check constraint can safely query arbitrary rows in other tables.
How should a backend define transaction boundaries and choose an isolation level?
Direct Answer
Place one local business invariant inside one short transaction, select isolation from the anomalies that must be prevented, and retry complete transactions when serialization conflicts occur.
Detailed Explanation
A transaction should normally represent one coherent database operation whose changes must commit or roll back together.
Good transaction boundaries are:
Isolation controls how concurrent transactions observe and affect one another. Possible anomalies include:
Higher isolation provides stronger coordination but can increase blocking or cause transactions to abort and require retrying.
The correct isolation level depends on the business rule. A read-only reporting query may tolerate a different level than an inventory reservation, account transfer, or uniqueness-sensitive workflow.
When a database reports a serialization failure or deadlock victim, the application should retry the entire transaction from the beginning. Retrying only the final statement may use stale decisions made earlier in the transaction.
External calls should generally not occur while a database transaction is held open because the dependency can delay completion or return an uncertain outcome.
Code Example
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT available_quantity
FROM inventory
WHERE product_id = 42;
UPDATE inventory
SET available_quantity = available_quantity - 1
WHERE product_id = 42
AND available_quantity > 0;
INSERT INTO reservations (
reservation_id,
product_id,
quantity
)
VALUES (
'reservation-91',
42,
1
);
COMMIT;Common Interview Pitfalls
- Keeping transactions open during remote API calls.
- Choosing serializable isolation without implementing transaction retries.
- Using several independent transactions for one local invariant.
- Retrying only the failed statement after a serialization conflict.
- Selecting an isolation level without identifying the anomaly being prevented.
- Holding locks while waiting for user input or background processing.
How should a backend developer design indexes and use query plans to diagnose slow database access?
Direct Answer
Design indexes from real filters, joins, ordering, and selectivity, then compare estimated and actual query-plan behavior under representative data before and after each change.
Detailed Explanation
Indexes provide alternative access paths that can avoid scanning every row, but they add storage, memory, and write-maintenance cost.
Index design should consider:
For a multicolumn B-tree index, leading-column conditions commonly determine the most efficient scan range. An index on (user_id, created_at) naturally supports queries that first constrain user_id and then order or filter by created_at.
EXPLAIN shows the planner’s estimated strategy. EXPLAIN ANALYZE executes the query and reports actual rows, loops, and timing.
Large differences between estimated and actual rows may indicate stale statistics, skewed data, correlated predicates, or parameter-sensitive behavior.
The presence of an index does not mean the database should always use it. For queries returning a large portion of a table, a sequential scan can be less expensive than many index lookups.
Code Example
CREATE INDEX applications_user_status_created_idx
ON job_applications (
user_id,
status,
created_at DESC
);
EXPLAIN (
ANALYZE,
BUFFERS
)
SELECT
id,
company,
created_at
FROM job_applications
WHERE user_id = 42
AND status = 'applied'
ORDER BY created_at DESC
LIMIT 25;Common Interview Pitfalls
- Adding an index to every column without considering write cost.
- Ignoring column order in a composite B-tree index.
- Looking only for whether an index appears in the execution plan.
- Testing query plans with unrealistic data volume or parameters.
- Keeping redundant and unused indexes indefinitely.
- Assuming a sequential scan always indicates poor performance.
How should a backend deploy a database schema change without breaking older application instances?
Direct Answer
Use expand-and-contract migrations: add compatible structures first, deploy code that supports both forms, backfill safely, switch reads and writes, then remove obsolete structures later.
Detailed Explanation
During a rolling deployment, old and new application versions may run at the same time. A schema migration must therefore remain compatible with both versions until the rollout is complete.
A common expand-and-contract sequence is:
1. Expand: Add the new column, table, index, or constraint in a compatible form.
2. Deploy code that can tolerate both old and new schemas.
3. Begin writing the new representation, sometimes through dual writes or a database-managed transition.
4. Backfill existing data in bounded batches.
5. Verify completeness and correctness.
6. Switch reads to the new representation.
7. Stop writing the old representation.
8. Contract: Remove the old column or table in a later deployment.
Large migrations should account for locks, table rewrites, transaction-log growth, replication lag, and application load.
Adding a required column with no transition plan can fail against existing rows or break old application versions. Similarly, renaming or dropping a column in one step can break instances that still reference it.
Every migration needs observability, a recovery plan, and a clear decision about whether rollback is safe after data transformation begins.
Code Example
-- Deployment 1: expand
ALTER TABLE users
ADD COLUMN display_name text;
-- Application supports both old and new representations.
-- Backfill in bounded batches.
UPDATE users
SET display_name = full_name
WHERE display_name IS NULL
AND id >= 1
AND id < 10001;
-- Later deployment, after verification:
ALTER TABLE users
ALTER COLUMN display_name SET NOT NULL;
-- A future deployment may remove full_name
-- after no running code depends on it.Common Interview Pitfalls
- Dropping or renaming a column while older application instances still use it.
- Backfilling an entire large table in one unbounded transaction.
- Adding a required field without handling existing rows.
- Deploying schema and application changes with no compatibility overlap.
- Using dual writes without monitoring divergence between representations.
- Assuming every data migration can be reversed safely.
How should services manage data ownership and consistency when one business operation spans multiple databases?
Direct Answer
Give each service authoritative ownership of its data, coordinate cross-service work through durable messages or sagas, and design explicitly for partial failure, retries, and temporary inconsistency.
Detailed Explanation
Independent services should normally own their persistence and expose behavior through APIs or events rather than allowing other services to modify their tables directly.
This ownership provides autonomy but makes one ACID transaction across several databases difficult or undesirable.
A distributed workflow can use a saga, which divides the business operation into local transactions. Each successful step triggers the next step, while failures may require compensating actions.
For example, placing an order might involve:
1. Creating a pending order.
2. Reserving inventory.
3. Authorizing payment.
4. Confirming the order.
5. Releasing inventory or voiding payment if a later step fails.
Compensation is not always a perfect rollback. An email cannot be unsent, and a refund is a new business action rather than erasing the original charge.
Reliable event publication can use a transactional outbox: the local state change and an outgoing event are written in the same database transaction, then a separate publisher delivers the event.
Consumers should be idempotent because messages can be delivered more than once. The workflow also needs explicit states for pending, failed, compensating, completed, and unknown outcomes.
Code Example
BEGIN;
UPDATE orders
SET status = 'inventory_reserved'
WHERE id = 'order-42'
AND status = 'pending';
INSERT INTO outbox_events (
event_id,
aggregate_id,
event_type,
payload
)
VALUES (
'event-91',
'order-42',
'InventoryReserved',
'{"orderId":"order-42"}'
);
COMMIT;Common Interview Pitfalls
- Allowing several services to write directly to one another’s tables.
- Assuming distributed calls will either all succeed or all fail together.
- Publishing an event separately from the database transaction that produced it.
- Designing compensating actions as if they perfectly erase history.
- Ignoring duplicate, delayed, or out-of-order message delivery.
- Exposing temporary inconsistent states without defining client behavior.
- Choosing a shard or service boundary that requires frequent distributed transactions.
What is the difference between vertical scaling and horizontal scaling?
Direct Answer
Vertical scaling increases the capacity of one machine, while horizontal scaling adds more instances and distributes work across them.
Detailed Explanation
Vertical scaling, sometimes called scaling up, increases the capacity of one resource by adding CPU, memory, storage, or network capability.
It can be attractive because:
Its limitations include hardware ceilings, larger failure impact, maintenance downtime, and increasingly expensive high-capacity machines.
Horizontal scaling, or scaling out, adds more application or worker instances and distributes work among them.
Horizontal scaling commonly requires:
Horizontal scaling improves aggregate capacity and can improve availability because one instance can fail while others continue serving traffic. However, it introduces distributed-system concerns such as network failures, duplicated work, consistency, and uneven load.
Many systems use both approaches. A database may first scale vertically, while stateless application servers scale horizontally behind a load balancer.
Common Interview Pitfalls
- Assuming horizontal scaling requires no application or data-design changes.
- Treating vertical scaling as inherently wrong for every workload.
- Adding application instances while leaving one unscaled downstream dependency.
- Keeping request-specific state only in local memory across several instances.
- Scaling from CPU utilization alone without examining latency, queues, or dependency saturation.
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, loads missing data from the source, and updates or invalidates cached entries when authoritative data changes.
Detailed Explanation
In the cache-aside pattern, the application manages the cache explicitly.
A typical read flow is:
1. Build a stable cache key.
2. Read the value from the cache.
3. If found, return the cached value.
4. If missing, read from the authoritative data store.
5. Store the result in the cache with an expiration policy.
6. Return the value.
A typical write flow updates the authoritative database and then invalidates or replaces the affected cache entry.
Invalidation is difficult because several events can occur concurrently. For example, one request may read an older database value while another updates the record. If the first request writes its stale value to the cache after the update, the cache becomes incorrect.
Common controls include:
A cache should normally be treated as disposable. If it becomes unavailable, the application should preserve correctness by falling back to the authoritative store, while protecting that store from a sudden surge of cache misses.
Code Example
final class UserProfileService {
private final Cache cache;
private final UserRepository users;
UserProfile find(UserId userId) {
String key = "user-profile:" + userId.value();
return cache.get(key, UserProfile.class)
.orElseGet(() -> {
UserProfile profile = users
.findProfile(userId)
.orElseThrow(UserNotFound::new);
cache.put(
key,
profile,
Duration.ofMinutes(10)
);
return profile;
});
}
void update(UserProfile profile) {
users.save(profile);
cache.delete(
"user-profile:" + profile.userId().value()
);
}
}Common Interview Pitfalls
- Treating the cache as the authoritative source of business data.
- Caching values without an expiration or invalidation strategy.
- Using inconsistent cache-key construction across readers and writers.
- Caching every query without considering reuse, size, or change frequency.
- Allowing simultaneous cache misses to overwhelm the underlying database.
- Assuming database updates and cache invalidation occur atomically.
How do load balancers and health checks improve backend scalability and availability?
Direct Answer
A load balancer distributes requests across eligible instances, while health checks remove instances that cannot safely serve traffic and restore them after recovery.
Detailed Explanation
A load balancer provides a stable entry point and distributes traffic across several backend instances.
It can support:
Health checks determine whether an instance should receive traffic. It is useful to distinguish:
A liveness check should not usually fail merely because a temporary downstream dependency is unavailable. Restarting every application instance during a shared database outage can worsen the incident.
A readiness check can be stricter because its purpose is to stop new traffic from reaching an instance that cannot serve it correctly.
Health endpoints should be inexpensive, bounded, and protected from disclosing sensitive configuration. They should not perform unbounded queries or make a long chain of remote calls.
Load balancing does not automatically make an application scalable. Shared databases, caches, rate limits, session state, and background workers must also support the increased traffic.
Code Example
@RestController
final class HealthController {
private final StartupState startupState;
@GetMapping("/health/live")
ResponseEntity<Void> live() {
return ResponseEntity.noContent().build();
}
@GetMapping("/health/ready")
ResponseEntity<Void> ready() {
if (!startupState.isReady()) {
return ResponseEntity
.status(HttpStatus.SERVICE_UNAVAILABLE)
.build();
}
return ResponseEntity.noContent().build();
}
}Common Interview Pitfalls
- Using one identical endpoint for liveness, readiness, and startup checks.
- Restarting healthy processes because a shared dependency is temporarily unavailable.
- Running expensive database queries during every health check.
- Returning secrets or sensitive infrastructure details from health endpoints.
- Sending traffic to an instance before initialization has completed.
- Assuming a load balancer removes the need for application-level overload protection.
How do message acknowledgements, redelivery, and idempotent consumers affect reliable processing?
Direct Answer
Acknowledgements transfer processing responsibility, but failures can cause redelivery. Consumers must therefore handle duplicate messages without repeating the business effect.
Detailed Explanation
A message broker separates producers from consumers and can retain work until a consumer is available.
Reliability commonly involves two different confirmations:
A consumer should acknowledge only after the required business work has completed durably. Acknowledging before processing risks message loss if the consumer crashes afterward.
Acknowledging after processing provides stronger protection, but duplicates remain possible. For example, the consumer may commit its database update and then lose the connection before its acknowledgement reaches the broker. The broker may redeliver the message.
An idempotent consumer can detect or safely tolerate that duplicate. Common techniques include:
Delivery labels such as at-most-once, at-least-once, and exactly-once must be interpreted across the entire workflow. A broker guarantee alone does not automatically make database or external side effects exactly once.
Code Example
BEGIN;
INSERT INTO processed_messages (
consumer_name,
message_id
)
VALUES (
'invoice-created-consumer',
'event-91'
)
ON CONFLICT DO NOTHING;
-- Continue only when the insert affected one row.
UPDATE customer_balances
SET invoiced_total = invoiced_total + 4999
WHERE customer_id = 'customer-42';
COMMIT;
-- Acknowledge the broker message only after commit.Common Interview Pitfalls
- Acknowledging a message before its business effect is committed durably.
- Assuming a broker guarantees exactly-once effects in an external database.
- Using a non-unique processed-message lookup that permits races.
- Retrying permanently invalid messages forever.
- Ignoring duplicate delivery after consumer crashes or network failures.
- Confusing publisher confirms with consumer acknowledgements.
How should a backend apply backpressure when incoming work exceeds processing capacity?
Direct Answer
Bound concurrency and queues, reject or defer excess work, expose overload signals, and scale consumers using queue depth, latency, and downstream capacity.
Detailed Explanation
Backpressure prevents producers from overwhelming consumers or downstream dependencies when arrival rate exceeds processing capacity.
Without backpressure, a system may continue accepting work into memory or unbounded queues until latency, memory usage, connection counts, and failure rates grow uncontrollably.
Useful controls include:
429 or 503A durable queue can absorb a temporary burst and allow workers to process at a controlled rate. It does not create unlimited capacity. If producers continuously generate work faster than consumers can process it, queue age and storage continue growing.
Backpressure should be applied near the overloaded resource. Increasing worker count beyond database, provider, or CPU capacity can make the bottleneck worse.
Important signals include queue depth, age of the oldest message, processing latency, rejection rate, active concurrency, downstream latency, and error rate.
Code Example
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
12,
12,
0,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
new ThreadPoolExecutor.AbortPolicy()
);
try {
executor.execute(task);
} catch (RejectedExecutionException overloaded) {
throw new ServiceUnavailableException(
"Processing capacity is temporarily exhausted"
);
}Common Interview Pitfalls
- Using an unbounded in-memory queue to hide overload.
- Scaling workers without checking database or dependency capacity.
- Monitoring only queue depth and ignoring message age.
- Retrying rejected work immediately without backoff or jitter.
- Accepting unlimited work because it has been moved to a durable queue.
- Applying one global limit without protecting high-priority or tenant-specific capacity.
How should a scalable event-driven backend handle event contracts, ordering, retries, and partial failure?
Direct Answer
Use durable publication, explicit event contracts, partitioned ordering, idempotent consumers, bounded retries, dead-letter handling, observability, and reconciliation for partial failures.
Detailed Explanation
An event-driven backend publishes facts about completed state changes and allows independent consumers to react asynchronously.
A robust design addresses several areas.
Event contracts
Events should have stable names, schemas, identifiers, occurrence times, producer information, and versioning rules. An event should represent something that happened, such as OrderConfirmed, rather than an implementation-specific method call.
Durable publication
A transactional outbox can store the state change and outgoing event in the same database transaction. A separate publisher later sends the event to the broker.
Ordering
Global ordering is expensive and often unnecessary. Systems commonly preserve ordering only within a partition key such as orderId or accountId. Consumers should not assume ordering across unrelated keys.
Duplicate delivery
At-least-once delivery is common, so consumers should use event IDs, conditional updates, or version checks to avoid repeating effects.
Failures
Transient failures may use bounded retries with backoff. Permanently invalid or repeatedly failing messages may move to a dead-letter destination for investigation and controlled replay.
Schema evolution
Changes should remain backward-compatible where possible. Consumers should tolerate additional fields and producers should avoid silently changing established field meaning.
Observability and recovery
Track publication lag, consumer lag, retry count, dead-letter volume, event age, processing latency, and correlation identifiers. Reconciliation jobs may be needed when events are missing, delayed, or processed inconsistently.
Code Example
BEGIN;
UPDATE orders
SET
status = 'confirmed',
version = version + 1
WHERE id = 'order-42'
AND status = 'payment_authorized';
INSERT INTO outbox_events (
event_id,
aggregate_id,
aggregate_version,
event_type,
occurred_at,
payload
)
VALUES (
'event-91',
'order-42',
7,
'OrderConfirmed',
now(),
'{
"orderId": "order-42",
"version": 7
}'
);
COMMIT;Common Interview Pitfalls
- Publishing an event separately from the transaction that changes authoritative state.
- Requiring global event ordering when only per-entity ordering is needed.
- Treating events as internal method calls rather than durable contracts.
- Retrying poison messages forever without a quarantine strategy.
- Changing existing event-field meaning without contract migration.
- Assuming every consumer processes an event exactly once.
- Creating events that expose the producer’s entire internal database model.
- Operating an event system without consumer-lag and dead-letter monitoring.
How do authentication, authorization, and least privilege differ in a backend system?
Direct Answer
Authentication establishes identity, authorization checks whether that identity may perform a specific action, and least privilege limits every identity to only the access it needs.
Detailed Explanation
Authentication determines who or what is making a request. Examples include a user session, access token, client certificate, or workload identity.
Authorization determines whether the authenticated identity may perform the requested action on the specific resource.
A backend may need to enforce several authorization dimensions:
Authentication alone is not permission. A signed-in user must not gain access to another user’s application merely by changing an identifier in the URL.
Least privilege means users, services, jobs, databases, and deployment systems receive only the permissions required for their current responsibilities.
Useful practices include:
Frontend visibility is not an authorization control. Hiding a button may improve usability, but the backend must still reject an unauthorized direct request.
Code Example
final class ApplicationAuthorization {
void requireReadAccess(
AuthenticatedUser actor,
JobApplication application
) {
boolean ownsApplication =
application.userId().equals(actor.userId());
boolean sameTenant =
application.tenantId().equals(actor.tenantId());
if (!ownsApplication || !sameTenant) {
throw new AccessDeniedException();
}
}
}Common Interview Pitfalls
- Treating authentication as permission to access every resource.
- Checking a broad role without verifying object ownership or tenant scope.
- Trusting a user or tenant identifier supplied by the client.
- Enforcing authorization only through hidden frontend controls.
- Giving service accounts broad administrator permissions for convenience.
- Failing to review permissions after responsibilities change.
How should a backend handle input validation, application secrets, and error responses securely?
Direct Answer
Validate untrusted data against explicit rules, keep secrets outside source code with controlled rotation, and return safe error contracts without exposing sensitive implementation details.
Detailed Explanation
All data crossing a trust boundary should be treated as untrusted, including:
Validation should occur as early as practical and should define expected type, length, range, format, allowed values, and structural relationships.
Allowlist-style validation is generally stronger than attempting to block every dangerous pattern. Validation does not replace safe APIs such as parameterized SQL queries or contextual output encoding.
Secrets such as database passwords, webhook keys, signing keys, and provider credentials should not be committed to source control or embedded in client applications. They should be obtained through a managed secret store or secure runtime configuration with:
Public errors should communicate a stable status and safe message. Internal logs may record more diagnostic context, but they still should not contain passwords, tokens, private keys, or unnecessary personal data.
Security-sensitive comparison, token validation, and cryptography should use established libraries rather than custom implementations.
Code Example
public CreateUserCommand validate(
CreateUserRequest request
) {
String email = request.email().trim();
if (
email.length() > 254 ||
!EMAIL_PATTERN.matcher(email).matches()
) {
throw new RequestValidationException(
"A valid email address is required"
);
}
if (request.displayName().length() > 100) {
throw new RequestValidationException(
"Display name is too long"
);
}
return new CreateUserCommand(
email,
request.displayName()
);
}Common Interview Pitfalls
- Using denylist validation as the primary defense against malformed input.
- Building SQL, shell commands, or queries through string concatenation.
- Committing credentials or signing keys to source control.
- Logging access tokens, passwords, or complete sensitive payloads.
- Returning stack traces or database errors to public clients.
- Rotating a secret without supporting controlled activation and rollback.
How do structured logs, metrics, and distributed traces work together in backend observability?
Direct Answer
Logs describe discrete events, metrics summarize measurable behavior over time, and traces follow requests across components. Shared context allows engineers to correlate all three.
Detailed Explanation
Observability helps engineers understand a backend’s internal behavior from its externally emitted telemetry.
The three common telemetry signals have different strengths:
Structured logs should use stable fields rather than embedding all information into free-form text. Useful fields can include:
Metrics are efficient for dashboards and alerting but often lack per-request detail. Traces identify where time was spent and which dependency failed, but sampling may mean not every request is retained.
Correlation is most valuable when a trace ID appears in logs and telemetry propagates across service boundaries. An engineer can begin with an alerting metric, inspect a representative trace, and then query logs from the affected spans.
Telemetry must avoid uncontrolled high-cardinality labels and sensitive data. Values such as raw user IDs, full URLs with tokens, or arbitrary exception messages can create cost, privacy, and operational problems.
Code Example
Span span = tracer.spanBuilder(
"CreateJobApplication"
).startSpan();
try (Scope ignored = span.makeCurrent()) {
metrics.counter("applications.created")
.add(1);
logger.info(
"Job application created applicationId={} traceId={}",
application.id(),
span.getSpanContext().getTraceId()
);
} catch (RuntimeException error) {
span.recordException(error);
span.setStatus(StatusCode.ERROR);
throw error;
} finally {
span.end();
}Common Interview Pitfalls
- Using free-form logs without stable fields or correlation identifiers.
- Adding high-cardinality values such as unrestricted user IDs to metric labels.
- Logging sensitive tokens or complete personal records.
- Collecting telemetry without defining how responders will use it.
- Assuming traces replace metrics or that metrics replace diagnostic logs.
- Failing to propagate trace context across asynchronous and remote boundaries.
How should health checks, service-level indicators, service-level objectives, and alerts be designed?
Direct Answer
Health checks control instance traffic, SLIs measure user-visible reliability, SLOs define acceptable targets, and alerts should fire when meaningful action is required.
Detailed Explanation
These mechanisms solve different operational problems.
Health checks determine whether an individual instance is alive, initialized, or ready to receive traffic.
Service-level indicators, or SLIs, quantify behavior users experience. Examples include:
A service-level objective, or SLO, defines the target for an SLI over a period. For example, 99.9% of eligible requests may need to succeed over 28 days.
The difference between perfect reliability and the chosen SLO creates an error budget. Teams can use that budget to balance feature delivery and reliability work.
Alerts should indicate a condition requiring timely human or automated action. Alerting on every temporary CPU spike produces noise and teaches responders to ignore pages.
Strong alerts include:
Burn-rate alerting measures how quickly an error budget is being consumed. Fast burn can detect severe incidents quickly, while slower windows detect sustained degradation.
Code Example
# Example availability SLI:
successful_requests / eligible_requests
# Example SLO:
99.9% successful requests over 28 days
# Error budget:
100% - 99.9% = 0.1%
# Exclude intentionally rejected invalid requests
# only when the SLI definition documents that policy.Common Interview Pitfalls
- Using infrastructure utilization as the only measure of user-visible reliability.
- Defining an SLO without specifying its measurement window and eligible events.
- Paging on conditions that require no immediate action.
- Making liveness depend on every temporary downstream failure.
- Setting every SLO to one hundred percent without evaluating cost and feasibility.
- Creating alerts without clear ownership, context, or a response procedure.
How should a backend degrade gracefully and recover when a dependency or component fails?
Direct Answer
Protect critical paths with timeouts and isolation, disable or defer noncritical work, preserve durable state, communicate degraded behavior, and reconcile incomplete operations after recovery.
Detailed Explanation
Graceful degradation allows a system to preserve its most important capabilities when part of the architecture is unavailable or overloaded.
Possible strategies include:
A fallback must preserve correctness. Returning an invented value or silently skipping a required payment check may be worse than failing the operation explicitly.
The system should define which features are critical, which may become read-only, and which can be delayed.
Recovery also requires reconciliation. A timeout does not always reveal whether a dependency completed an operation. The backend may need to query the provider, consume a later webhook, or run a scheduled reconciliation job.
Operational recovery should include controlled retrying, queue draining, replay procedures, and monitoring for residual inconsistency. Restoring traffic too quickly can overload a recovering dependency and cause repeated failure.
Code Example
RecommendationResult recommendations(
UserId userId
) {
try {
return recommendationClient.fetch(
userId,
Duration.ofMillis(300)
);
} catch (
TimeoutException |
CircuitOpenException unavailable
) {
metrics.increment(
"recommendations.degraded"
);
return RecommendationResult.unavailable();
}
}Common Interview Pitfalls
- Using a fallback that returns incorrect or unsafe business results.
- Retrying a recovering dependency with uncontrolled concurrency.
- Treating an uncertain external result as a confirmed failure.
- Failing the entire product because one optional feature is unavailable.
- Queueing deferred work without capacity limits or expiration policies.
- Restoring full traffic before verifying that the dependency has recovered.
How would you design a backend that is secure, observable, reliable, and operable in production?
Direct Answer
Use explicit trust boundaries, least privilege, layered validation, resilient dependencies, durable workflows, correlated telemetry, measurable SLOs, controlled deployment, and tested recovery procedures.
Detailed Explanation
Production readiness is a system property rather than one library or infrastructure feature.
A strong design addresses several areas.
Security boundaries
Reliability
Observability
Deployment safety
Operations
The design should identify critical user journeys and ensure their security, reliability, telemetry, and recovery characteristics are understood before production launch.
Common Interview Pitfalls
- Treating security and observability as features to add after implementation.
- Collecting large volumes of telemetry without actionable service objectives.
- Using retries without deadlines, idempotency, or downstream protection.
- Giving services shared administrator credentials across environments.
- Deploying breaking database changes in the same step as application code.
- Creating dashboards without defining incident ownership or response actions.
- Testing backups without verifying that restoration actually works.
- Designing fallbacks that violate correctness or security requirements.
Want to tailer your resume for Backend Developer roles?
Import your resume, scan it for critical Backend Developer keywords, and compare it against ATS standards instantly.