cswork / libraryOpen practice workspace

Interview guides · BridgeNexus Technologies

BridgeNexus Technologies Software Engineer Interview Guide

BridgeNexus Technologies Software Engineer interview guide: reported rounds, Java and Spring Boot questions, binary search drills and a 7-day plan.

The 60-second brief

Candidates describe BridgeNexus Technologies as a company whose Software Engineers build and maintain the backend services behind its platforms. Reports do not name specific products or customers, so this guide sticks to what is reported about the work itself. Engineers on the role are described as taking part in the whole development cycle: writing testable code, joining code reviews, designing new features with their team, troubleshooting production issues and tuning the performance of existing systems. The stack candidates mention is Java with the Spring ecosystem (Spring Boot, SpringMVC, Spring Data), applied to microservices, with cloud infrastructure, CI/CD pipelines and Docker listed as extras rather than requirements.

This guide is for a Software Engineer candidate preparing for BridgeNexus Technologies, especially someone whose background is Java and Spring.

Candidates report three rounds over roughly 3-5 weeks: a Technical Screening Call, a Technical Assessment, and a Behavioral and System Design round.

Focus areas

  • Java
  • Binary Search Algorithm
  • Spring Boot

Overview

The role, as candidates describe it, is day-to-day backend engineering on Java and Spring services. You write code that has to be tested and reviewed, you help design features, you diagnose production problems and you tune slow code. That shapes the preparation: the questions reported for this loop ask how the framework works (dependency injection, the SpringMVC request flow, bean lifecycles, Spring Data compared with JDBC, choosing between Java collections), so naming annotations is not enough. You need to explain what the container and the dispatcher do, and to have run a small Spring Boot application yourself.

The second reported group is algorithmic: walking through a binary search, choosing a structure to speed up search over a given data set, working out time and space complexity for a function, and handling an edge case in a standard sorting algorithm. The reported evaluation areas add HashMaps, trees and queues, and Big O reasoning used to justify choices. Practise writing these in Java, stating the complexity unprompted and listing the inputs that break your first version: empty, single element, duplicates, already sorted, reverse sorted.

The third round is named Behavioral and System Design. Candidates report that for this role the emphasis is on implementing features in Spring and solving algorithmic problems, with architecture discussed at a high level where it fits your experience. So prepare two things: project stories with a decision, a trade-off and a result, and one service you can sketch end to end (controller, service layer, repository, database) while explaining why you shaped it that way. Reported advice also points to evolving requirements, handling feedback and explaining trade-offs to product colleagues, so have a story for each.

Nothing reported separates the three rounds by question, so treat every technical subject as fair game in the assessment and keep your stories ready for the last round. Because candidates are told to speak their reasoning while coding, rehearse out loud: a correct solution you cannot narrate is weaker preparation than a slightly slower one you can explain.

The BridgeNexus Technologies interview process

Candidates report three rounds in the order below, spread over about 3-5 weeks depending on team availability. The reports do not say which question comes in which round, so the guidance below prepares question types, not a fixed script.

Technical Screening Call

Candidates describe this first stage as a call that covers your background and checks how your expertise lines up with what a team currently needs. Reports do not list specific questions for it, so prepare to talk about your own work: the Java and Spring projects you have shipped, which parts you owned, and which of the stated requirements (Java, Spring, data structures and algorithms) you can back with a concrete example. Aim for a tight, concrete account of your experience rather than a recital of your CV.

What to demonstrate

  • How your background maps onto the team's current needs, as the stage is described
  • How clearly you summarise projects you built and maintained
  • Familiarity with the Java and Spring stack the role lists

How to prepare

  • Write a short account of your last two projects naming the Spring modules or Java features you used, your own contribution and one measurable result.
  • Pick two framework topics from the reported list, for example dependency injection and Spring Data against JDBC, and prepare a brief explanation grounded in something you built.
  • Write two questions about the team's current technical work to ask at the end of the call.

Technical Assessment

Candidates describe this stage as the one that assesses technical capability and fit with the engineering team. The technical questions reported for the loop fall into two groups: Java and Spring framework questions (dependency injection in Spring Boot, the SpringMVC request flow, bean lifecycles, Spring Data against JDBC, Java collection types) and algorithm questions (binary search, optimizing search over a data set, time and space complexity, sorting edge cases). Reports do not tie individual questions to a stage, so prepare both groups, explain your reasoning aloud as you write Java, and state complexity as you go.

What to demonstrate

  • Working knowledge of Java and the Spring ecosystem beyond surface-level usage
  • Breaking a problem into parts and justifying data structure choices with Big O
  • Edge case handling in the code you write

How to prepare

  • Build a small Spring Boot application with a controller, a service and a repository so you can describe bean creation, injection and the request path from experience.
  • Write binary search and one sorting routine in Java from memory, then list the edge cases you would test for each.
  • Say the time and space complexity of every function you write during practice before anyone asks, including the recursion stack where it applies.

Behavioral and System Design

Candidates describe discussions that combine behavioral topics with system design. Reports add that the technical emphasis for this role sits on Spring feature work and algorithms, with architecture raised at a high level depending on your experience. Prepare project stories that show a decision, its trade-off and its outcome, plus one design of a Spring-based service you can sketch and defend from work you have actually done.

What to demonstrate

  • Behavioral judgment: handling feedback, changing requirements and project challenges
  • High-level design reasoning around a service built on the Spring stack
  • How you explain technical trade-offs to non-engineering colleagues

How to prepare

  • Write the story of a refactor that improved algorithmic efficiency: the original complexity, the change, how you measured the result.
  • Sketch a Spring Boot service with its layers and database access, and rehearse why you chose Spring Data or JDBC for it.

Where candidates lose points

Answering Spring questions with annotation names instead of mechanics

For dependency injection, say that the container builds the beans, resolves constructor arguments by type and hands out singletons by default. For SpringMVC, name the order: servlet filters, the DispatcherServlet, handler mapping, interceptors, the controller, message conversion, then exception handling. Run a small app with logging at each point so the order is something you have seen.

Praising Spring Data over JDBC without naming what it costs

Give both sides: repositories remove mapping and query boilerplate and add paging, but the SQL becomes less visible. Mention the surprises, such as extra queries from lazy relationships, and say when you would still write a query by hand. Turn on SQL logging in your practice app so your claims come from output.

Off-by-one and overflow mistakes in binary search

Decide your invariant before coding (for example, the answer lies within lo..hi inclusive), compute the midpoint as lo + (hi - lo) / 2, and walk through inputs of length 0, 1 and 2, a target below the range, above it, and duplicates. Be ready to adapt the loop to find the first occurrence or an insertion point.

Stating complexity as a label with no derivation, or missing hidden costs

Count loop iterations and the cost of each call inside them. Watch for list.contains inside a loop, ArrayList removal from the front, string concatenation in a loop and recursion depth. Quote space separately from time, and say which input shape produces the worst case.

Telling the refactoring story with no before and after

Open with the original behaviour and its complexity, name the specific change (a HashMap index, a sorted structure with binary search, removing a nested loop), then give the measured result and how you verified correctness. A story with no numbers sounds like a guess.

Questions & practice

Walk me through the implementation of a binary search algorithm.

Medium · Algorithms & Problem Solving

Approach

  1. State the precondition: the input is sorted, or more generally a predicate is monotonic over the index. Keep lo and hi, and compute mid as lo + (hi - lo) / 2 (or (lo + hi) >>> 1) so that lo + hi cannot overflow an int. Compare a[mid] with the target and move lo = mid + 1 or hi = mid - 1.
  2. Give the invariant and termination argument. With a closed range [lo, hi] and while (lo <= hi), the target, if present, is always inside the range, and the range shrinks every iteration. The iterative version is O(log n) time and O(1) space. A recursive one uses O(log n) stack.
  3. Handle duplicates with lower bound. Use a half-open range [lo, hi), loop while (lo < hi), and set hi = mid when a[mid] >= target. The result is the first index whose value is at least the target. Upper bound is the same with >. The number of occurrences is upper minus lower.
  4. Edge cases: an empty array, a single element, a target below the minimum or above the maximum, and an array of all-equal values. Java's Arrays.binarySearch returns -(insertionPoint) - 1 when the key is missing and does not say which duplicate it finds.
  5. Extend the idea. Binary search on the answer (smallest capacity that ships everything within D days) and search in a rotated sorted array (decide which half is sorted) both use the same loop with a different predicate.

Follow-up

  • Return the first and last index of the target in a sorted array with duplicates.
  • Search for a target in a rotated sorted array without first finding the pivot separately.
  • Why can (lo + hi) / 2 fail in Java, and what does >>> 1 change?

Given a specific data set, how would you optimize for search performance?

Medium · Algorithms & Problem Solving

Approach

  1. Ask before choosing. How large is the data? Is it static or frequently updated? Are queries exact-key, range, prefix, full-text or nearest-neighbour? What is the read/write ratio, the memory budget and the latency target?
  2. In memory: a HashMap gives O(1) average exact lookups after an O(n) build. A sorted array with binary search costs O(n log n) to sort once, then O(log n) per query, and also supports ranges. A TreeMap gives O(log n) inserts and ordered or range queries when the data changes. Use a trie or a sorted array with lower bound for prefixes, and an inverted index for text.
  3. In a database, add a B-tree index on the filtered column. In a composite index, put equality columns before the range column. Avoid wrapping indexed columns in functions, and confirm the plan with EXPLAIN (ANALYZE) instead of assuming the index is used.
  4. For repeated queries, add a cache (an LRU map, Caffeine, or Spring @Cacheable), and state the invalidation rule and the staleness you accept. Precomputing frequent answers is another option.
  5. Measure before and after with a JMH benchmark or production latency percentiles. State the cost you take on: extra memory, slower writes to maintain the index, and longer build time.

Follow-up

  • How does your choice change if the data receives thousands of updates per second?
  • What if the data set no longer fits in memory on one machine?
  • How would you support both exact lookups and range queries on the same key?

How do you determine the time and space complexity of a given function?

Medium · Algorithms & Problem Solving

Approach

  1. Count the dominant work as a function of input size. Sequential blocks add, and nested loops multiply by their real bounds: an inner loop from i to n gives n(n+1)/2, which is O(n^2). A loop that halves or doubles its variable is O(log n). Drop constants and lower-order terms.
  2. For recursion, write the recurrence. T(n) = 2T(n/2) + O(n) is O(n log n) by the Master theorem, and T(n) = T(n-1) + O(1) is O(n). Naive Fibonacci, T(n) = T(n-1) + T(n-2), grows exponentially, and memoisation makes it O(n) time and O(n) space.
  3. Space is the auxiliary memory you allocate plus the depth of the recursion stack. Recursive DFS uses O(h). Merge sort needs O(n) extra space. Quicksort needs O(log n) stack on average and O(n) in the worst case unless you recurse on the smaller side.
  4. Count the hidden library costs. list.contains or ArrayList.remove(0) inside a loop is O(n) per call. Concatenating Strings in a loop is O(n^2) in total, so use a StringBuilder. ArrayList.add is amortised O(1) because of geometric growth.
  5. Say which case you mean: best, average or worst. A HashMap lookup is O(1) on average, and quicksort is O(n log n) on average but O(n^2) in the worst case. With two inputs, keep both variables (O(n + m) or O(n * m)) instead of collapsing them into n.

Follow-up

  • What is the time and space complexity of a recursive function once you add memoisation?
  • Why is appending to an ArrayList amortised O(1) when some appends copy the whole array?
  • A function loops over list A and calls contains on list B. What is its complexity, and how do you lower it?

Can you explain how you would handle an edge case in a standard sorting algorithm?

Medium · Algorithms & Problem Solving

Approach

  1. List the inputs to test: empty, one element, already sorted, reverse sorted, all equal or heavily duplicated, containing nulls, very large, and (for floating point) NaN and -0.0.
  2. For quicksort, sorted input with a first or last-element pivot degrades to O(n^2). Fix it with a random or median-of-three pivot. Many duplicates call for three-way partitioning (Dutch national flag). Bound the recursion depth by recursing on the smaller side, or fall back to heapsort as introsort does.
  3. Address stability. Merge sort, insertion sort and TimSort are stable. Quicksort and heapsort are not. In Java, Arrays.sort on primitives uses dual-pivot quicksort, where stability does not matter. Arrays.sort on objects and Collections.sort use TimSort, which is stable, O(n) on input that is already sorted and O(n log n) in the worst case.
  4. Check the comparator. Never compare with a - b, which can overflow; use Integer.compare or Comparator.comparing. A comparator that is not transitive can make TimSort throw IllegalArgumentException ('Comparison method violates its general contract'). Handle nulls with Comparator.nullsFirst or nullsLast. Double.compare puts -0.0 before 0.0 and NaN after everything else.
  5. Verify with randomised tests that compare against a known-good sort, include duplicate-heavy and adversarial inputs, and check stability by sorting records that carry their original index.

Follow-up

  • How would you sort a file too large to fit in memory (external merge sort)?
  • How do you sort by one key and then a second key, and why does stability matter there?
  • Why does Java use a different algorithm for primitive arrays than for object arrays?

Describe a situation where you had to refactor code to improve its algorithmic efficiency.

Medium · Algorithms & Problem Solving

Approach

  1. Open with a measured symptom: the system, the input size and the number that was wrong (p99 latency, CPU, job runtime). Then say how you located the bottleneck: a profiler (JFR, async-profiler), the slow query log or EXPLAIN, rather than a guess.
  2. Name the complexity change concretely. Examples: a nested membership check that was O(n * m) replaced with a HashSet for O(n + m); an N+1 query loop replaced by one batched IN query or a join; repeated full sorts replaced by a size-k heap for O(n log k).
  3. Explain how you kept the behaviour unchanged: characterisation tests written before the change, a comparison of old and new output on real data, a JMH or load benchmark, and a feature flag or staged rollout.
  4. Give the measured result and the cost you accepted (extra memory for the set or cache, more code to maintain), in first person singular so your own part is clear.
  5. The answer fails if the improvement is claimed without a before/after number, if the bottleneck was guessed, or if 'the team' did everything and your own decision cannot be identified.

Follow-up

  • How did you confirm the refactor did not change results for edge-case inputs?
  • What did the faster version cost in memory or readability, and was it worth it?
  • If the input grew a hundredfold, which part of your solution would become the bottleneck next?

Keep soft-deleted accounts from blocking re-registration

Easy · soft delete · unique constraints · engine differences

app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.

Approach

  1. Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
  2. Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
  3. For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
  4. State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.

Follow-up

  • A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
  • How do you honour an erasure request while resource_revision.actor_user_id still references this table?

Find version gaps and relay lag with window functions

Medium · window functions · event tables · frame semantics

outbox_event holds event_id, aggregate_type, aggregate_id, aggregate_version, event_type, payload, status ('pending','published','dead'), attempts, created_at, published_at. A projection is missing rows and you must decide whether the relay skipped events or the consumer dropped them. Write three queries over the last seven days: one listing every aggregate_id whose published aggregate_version sequence has a hole, one giving per-day counts with a running total, and one returning the newest published event per aggregate. For each, say where the window function is evaluated relative to WHERE and LIMIT. PostgreSQL 16.

Approach

  1. Gaps: compute lead(aggregate_version) OVER (PARTITION BY aggregate_id ORDER BY aggregate_version) in a subquery, then filter next_version <> aggregate_version + 1 in the outer query. Window functions are evaluated after WHERE, GROUP BY and HAVING and before the outer ORDER BY and LIMIT, so the predicate cannot sit in the same WHERE clause and PostgreSQL 16 has no QUALIFY.
  2. Say what the seven-day filter does to the answer: it truncates every partition, so the first row per aggregate has no predecessor inside the window and a hole spanning the boundary is invisible. Widen the window, or join to resource.version as the authority for the true maximum.
  3. Running total: SELECT date_trunc('day', created_at) AS d, count() AS n, sum(count()) OVER (ORDER BY date_trunc('day', created_at) ROWS UNBOUNDED PRECEDING). An aggregate inside a window call is legal because grouping runs before windowing. The grouping key is unique per row here so ROWS and RANGE agree, but write the frame anyway — over ungrouped rows with tied timestamps the default RANGE frame pulls in every peer row and the total jumps.
  4. Newest per aggregate: DISTINCT ON (aggregate_id) ... ORDER BY aggregate_id, aggregate_version DESC is the cheap PostgreSQL-only form when an index matches that order; row_number() OVER (PARTITION BY aggregate_id ORDER BY aggregate_version DESC) = 1 is the portable form and needs a subquery for the same evaluation-order reason as the gap query.

Follow-up

  • Relay failover redelivers events. Does a duplicate break the gap query, and how would you detect one from this table alone?
  • Turn the gap check into a continuous monitor rather than a query someone runs after an incident. What does it watch?

Explain the core concepts of Spring Boot and how it simplifies dependency injection.

Medium · Technical Proficiency & Frameworks

Approach

  1. Separate the two layers. The Spring Framework IoC container (ApplicationContext) creates beans and injects them. Spring Boot adds starters, auto-configuration (@SpringBootApplication includes @EnableAutoConfiguration and @ComponentScan), an embedded server and externalised configuration through application.properties/yml and @ConfigurationProperties.
  2. Explain how auto-configuration works. Conditional configuration classes (@ConditionalOnClass, @ConditionalOnProperty, @ConditionalOnMissingBean) register beans based on what is on the classpath. Since Boot 2.7 they are listed in META-INF/spring/...AutoConfiguration.imports instead of spring.factories.
  3. Show the DI mechanics. Component scanning starts from the main class's package and picks up @Component, @Service, @Repository and @Controller classes. Prefer constructor injection with final fields: a single constructor needs no @Autowired since Spring 4.3, and the class can be unit-tested without a container.
  4. Cover ambiguity and overrides. Two beans of one type cause NoUniqueBeanDefinitionException, which you resolve with @Primary or @Qualifier, or by injecting List<T>. If you define your own bean (an ObjectMapper or DataSource, say), @ConditionalOnMissingBean makes Boot's auto-configured one back off.
  5. Edge cases: since Boot 2.6, circular references are prohibited by default, and constructor injection exposes them at startup. To see why a bean was or was not created, run with --debug (the condition evaluation report) or use the Actuator conditions endpoint.

Follow-up

  • How would you replace an auto-configured bean, and how would you confirm Boot backed off?
  • Why is constructor injection preferred over field injection, and what does it change for testing?
  • An interface has two implementations and both are beans. How does Spring choose, and how do you control it?

Describe the request-response lifecycle within a SpringMVC architecture.

Medium · Technical Proficiency & Frameworks

Approach

  1. Start at the container. A thread from the embedded Tomcat pool takes the request, which passes through the servlet Filter chain (Spring Security's FilterChainProxy sits here via DelegatingFilterProxy) before it reaches the DispatcherServlet.
  2. Next comes the DispatcherServlet. It asks its HandlerMappings for a handler. RequestMappingHandlerMapping matches @RequestMapping on path, HTTP method and consumes/produces, and returns a HandlerExecutionChain. Each HandlerInterceptor's preHandle runs, and if one returns false the request stops there.
  3. The RequestMappingHandlerAdapter invokes the controller method. Argument resolvers bind @PathVariable, @RequestParam and @RequestBody; the request body is read by an HttpMessageConverter such as Jackson's. @Valid failures raise MethodArgumentNotValidException.
  4. On the way out, @ResponseBody and @RestController return values are written by a message converter chosen through content negotiation, and that happens before postHandle runs. A view name goes to a ViewResolver and a View renders it instead. afterCompletion runs for every interceptor whose preHandle returned true.
  5. Errors go through HandlerExceptionResolvers: @ExceptionHandler methods (including in @ControllerAdvice), then @ResponseStatus, then DefaultHandlerExceptionResolver. Exceptions thrown inside a Filter never reach @ControllerAdvice.

Follow-up

  • Would you put authentication, request-ID logging and response timing in a Filter or in a HandlerInterceptor, and why?
  • Why can't postHandle change a JSON response body, and what would you use instead (ResponseBodyAdvice)?
  • Under thread-per-request, what happens when a downstream call slows down, and what options do you have (async handlers, virtual threads in Boot 3.2+)?

How do you manage bean lifecycles in a Java-based application?

Medium · Technical Proficiency & Frameworks

Approach

  1. Give the order. First the constructor, then property and field injection, then the Aware callbacks (BeanNameAware, ApplicationContextAware). Then BeanPostProcessor before-initialisation, which is where @PostConstruct runs. Then afterPropertiesSet, then the custom initMethod, then after-initialisation, which is where AOP proxies are created. On context close: @PreDestroy, DisposableBean.destroy, then destroyMethod.
  2. Cover scopes. Singleton is the default (one per container), and prototype gives a new instance per lookup and the container never calls its destroy callbacks. request, session and application are web scopes. To use a shorter-lived bean inside a singleton, inject ObjectProvider<T>, use @Lookup or use a scoped proxy (proxyMode = TARGET_CLASS).
  3. Say where startup work belongs. Use @PostConstruct for local checks on the bean itself. Use ApplicationRunner/CommandLineRunner or an ApplicationReadyEvent listener for work that needs the whole context. Use SmartLifecycle with phases when start and stop order matters.
  4. State the eager vs lazy trade-off. Singletons are created at startup, so wiring errors fail fast. @Lazy or spring.main.lazy-initialization=true starts up faster but pushes those failures to the first request.
  5. Pitfall: @Transactional or @Async on a method called from @PostConstruct of the same bean does not apply, because a self-call bypasses the proxy. In Boot 3 the lifecycle annotations come from jakarta.annotation, not javax.

Follow-up

  • Why does a @Transactional method called from @PostConstruct of the same bean run without a transaction?
  • How do you release resources held by a prototype-scoped bean, given the container does not destroy it?
  • How would you make a service finish in-flight work on shutdown (graceful shutdown, SmartLifecycle stop)?

Can you discuss the advantages of using Spring Data over standard JDBC?

Medium · Technical Proficiency & Frameworks

Approach

  1. Name the costs of plain JDBC: Connection, PreparedStatement and ResultSet boilerplate, try-with-resources for every resource, checked SQLException, and mapping each row by hand. Note that JdbcTemplate already removes most of this and translates errors into the DataAccessException hierarchy.
  2. List what Spring Data adds on top. A repository interface extends JpaRepository and gets CRUD for free. Queries can be derived from method names (findByEmailAndStatus) or written with @Query. It also provides Pageable with Page/Slice, projections and auditing (@CreatedDate). SimpleJpaRepository is @Transactional(readOnly = true) by default, and its write methods override that.
  3. Give the trade-offs honestly. JPA hides the SQL, so lazy associations cause N+1 queries, which you fix with JOIN FETCH, @EntityGraph or batch fetching. The persistence context grows during bulk work. Page runs an extra count query, and Slice avoids it.
  4. Say when you drop down a layer: bulk inserts and updates (JdbcTemplate.batchUpdate), reporting queries with complex joins or window functions (native SQL), and hot paths where you need exact control of the statement. Spring Data JDBC is a lighter middle option with no lazy loading and no persistence context.
  5. Show how you verify it. Log the generated SQL (logging.level.org.hibernate.SQL), count the statements per request in a @DataJpaTest, and check the plans with EXPLAIN.

Follow-up

  • A list endpoint issues one query per row. How do you find the N+1 and fix it?
  • When would you choose a native query or JdbcTemplate over a derived repository method?
  • How does offset pagination with Page compare with keyset pagination on a large table?

What are the key differences between various Java collection types and when should they be used?

Medium · Technical Proficiency & Frameworks

Approach

  1. Lists: ArrayList is array-backed, with O(1) get and amortised O(1) append (it grows by about 1.5x) but O(n) insert or remove in the middle. LinkedList is O(n) for get and O(1) only at the ends or at an iterator position, and its cache locality is poor. For stacks and FIFO queues, use ArrayDeque instead of LinkedList or Stack.
  2. Maps and sets: HashMap and HashSet are O(1) on average, unordered, and depend on correct equals/hashCode. Since Java 8 a bucket with more than 8 entries is turned into a tree once the table has at least 64 slots, which bounds the worst case at O(log n). LinkedHashMap keeps insertion or access order, and access order plus removeEldestEntry gives an LRU cache. TreeMap is a red-black tree with O(log n) operations, sorted iteration and floorKey/ceilingKey/subMap.
  3. Priority and concurrency: PriorityQueue is a binary heap with O(log n) offer and poll and O(1) peek, and iterating it does not return sorted order. ConcurrentHashMap allows no null keys or values. CopyOnWriteArrayList suits read-mostly lists. A BlockingQueue (ArrayBlockingQueue, LinkedBlockingQueue) handles producer-consumer.
  4. Edge cases: changing a key's hashCode after insertion makes it unfindable. Modifying a collection inside a for-each throws ConcurrentModificationException, so use Iterator.remove or removeIf. List.of is immutable and rejects nulls. Arrays.asList is fixed-size. Collections.synchronizedList still needs manual locking while you iterate it.
  5. Close with a decision rule: choose from the access pattern (lookup by key, ordered or range queries, FIFO, priority) and the threading model, then state the complexity of the operation that dominates.

Follow-up

  • Implement an LRU cache with the standard collections. Which class do you use, and which method do you override?
  • What happens to a HashMap entry whose key is mutated after insertion?
  • Compare HashMap, Hashtable, Collections.synchronizedMap and ConcurrentHashMap for a shared cache.

Exports duplicate a row range about once a week

Hard · leases · clock skew · idempotency

Roughly once a week an export writes a file containing a duplicated range of rows. The affected job_run rows show attempt = 1, status = succeeded, one started_at, and a lease_owner naming a different host from the one whose logs show the job starting. Leases last 30 seconds and are heartbeated every 10 from inside the handler; lease_expires_at is computed on the worker and compared against the database's now(). Find the mechanism, and give a fix that holds even if you cannot fix the clocks.

Approach

  1. Start from the fact that eliminates the obvious answer. attempt = 1 means no retry was recorded, so this is not a re-run after failure; two workers ran the same row concurrently and the takeover path never touched the counter. lease_owner naming a host other than the one that started the job is the same statement from the other side.
  2. Enumerate the mechanisms that cause a premature takeover, then find the signal that separates them. Either the lease genuinely expired because the heartbeat did not fire, which is what happens when the heartbeat runs on the handler's own thread and the handler makes a long blocking call, or it only appeared expired because two clocks disagree, since lease_expires_at is written from the worker's clock and evaluated against the database's. The discriminator is the distribution: incidents clustered on the longest exports indict the heartbeat, incidents clustered on one host indict skew. Measure both, and measure each host's offset against the database directly.
  3. Read the reclaim query precisely. In PostgreSQL now() is transaction start time, not statement time, so a reclaimer holding a long transaction compares against an older timestamp than expected; clock_timestamp() is the statement-time function. This is worth ruling in or out before you redesign anything, because it changes which rows look expired.
  4. Remove the second clock rather than trying to synchronise it. Issue and extend the lease in the database, with lease_expires_at = now() + interval '30 seconds' in both the claim and the heartbeat, so exactly one clock is ever compared and worker skew stops mattering to this predicate.

Follow-up

  • The displaced worker has already streamed half the file to object storage. What makes that side effect safe to repeat?
  • You now count takeovers. What alert fires on that counter, and at what threshold?

Suggested practice checklist

Seven days that move from the loop and your baseline to Spring mechanics, then Java collections and algorithms, then the stories and design sketch for the last round. Each day ends with something you produced, and practice questions are named by subject.

Day 1 — Map the loop and rate your baseline

  • Write the three reported rounds in order and beside each note what you would need ready for it.
  • Rate your confidence on the ten reported question subjects (Spring Boot injection, SpringMVC flow, bean lifecycles, Spring Data against JDBC, Java collections, binary search, search optimization, complexity analysis, sorting edge cases, the refactoring story).
  • Write a short account of your Java and Spring background that you could say on a screening call.

Deliverable: A one-page loop map, a confidence table covering all ten reported subjects, and a written background summary.

Day 2 — Spring Boot, injection and bean lifecycle

  • Build a small Spring Boot application with a controller, a service and a component that uses constructor injection.
  • Add a bean with initialization and destruction callbacks and log the order in which they run on startup and shutdown.
  • Explain aloud what @SpringBootApplication and auto-configuration do, and answer the Spring Boot dependency injection question and the bean lifecycle question.

Deliverable: A running application, a log showing callback order, and spoken answers to the Spring Boot injection and bean lifecycle questions.

Day 3 — SpringMVC flow and Spring Data against JDBC

  • Add a filter, an interceptor and an exception handler to your app and log each one to see the request path in order.
  • Write the same read and write once with JdbcTemplate and once with a Spring Data repository, with SQL logging on, and note what differs.
  • Answer the SpringMVC lifecycle question and the Spring Data against JDBC question aloud.

Deliverable: A log of the request path, two side-by-side data access versions with the SQL each produced, and spoken answers to both questions.

Day 4 — Java collections and complexity analysis

  • Build a table for ArrayList, LinkedList, ArrayDeque, HashMap, LinkedHashMap, TreeMap, HashSet, PriorityQueue and ConcurrentHashMap covering lookup, insert, removal, ordering, null handling and thread safety.
  • Annotate five short functions with time and space complexity, including any recursion stack.
  • Answer the Java collections question and the time and space complexity question aloud.

Deliverable: A completed collections table, five annotated functions, and spoken answers to the two questions.

Day 5 — Binary search and sorting edge cases

  • Write iterative binary search in Java from memory, then the first-occurrence and insertion-point variants.
  • Make a test table for it: empty, one element, two elements, target below range, above range, duplicates.
  • Write a merge sort or quicksort and list its edge cases: empty, single element, all equal, already sorted, reverse sorted, duplicates, stability.
  • Answer the binary search walkthrough question and the sorting edge case question aloud.

Deliverable: A working binary search with two variants and its test table, one sorting routine with an edge-case checklist, and spoken answers.

Day 6 — Search optimization and the refactor story

  • Take one data set and compare answering lookups by scanning, by a HashMap index, by a sorted array with binary search and by a TreeMap, timing each on a large input.
  • Write a decision table that maps access patterns (exact lookup, range, repeated reads, frequent writes) to a structure.
  • Write out the refactoring story with original complexity, the change and the measured result, and answer the search optimization question aloud.

Deliverable: Timing results, a decision table, a written refactoring story with numbers, and a short diagnosis write-up for the debugging exercise.

Day 7 — Behavioral and system design dry run

  • Sketch a Spring Boot service from your own experience (layers, database access, how a new requirement would change it) and explain the trade-offs aloud.
  • Run the three rounds back to back as a mock, then finalise two questions to ask the team.

Deliverable: A one-page service sketch, recorded answers to the three behavioral exercises, and two written questions for the team.

Project stories for the Behavioral and System Design round

Candidates report a round that mixes behavioral discussion with system design. Reported advice for this loop points at your past projects and the technical decisions behind them, handling feedback, adjusting when requirements shift, and explaining trade-offs to people outside engineering. Build each story around a decision, the alternative you rejected, and a result you can measure.

  1. Describe a situation where you had to refactor code to improve its algorithmic efficiency. State the original complexity, what you changed, how you proved the result was correct, and the measured improvement.
  2. Tell me about a hard problem in a past project and the technical decision you made to get through it. Name the options you weighed and what you gave up.
  3. Describe a time requirements changed while you were building a feature. What did you rework, what did you keep, and how did you keep quality up?
  4. Tell me about review feedback that you disagreed with or found hard to hear. What did you do next, and what changed in the code or in how you work?
  5. Describe a time you explained a technical trade-off to a product manager or other non-engineer. What did you choose, what did you tell them it would cost, and how did they decide?
  6. Tell me about a tool or framework you had to pick up with little experience. How did you become productive, and what mistakes did you make early?

Argue against a design, lose, and commit anyway

Medium · disagreement · service boundaries · decision records

Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.

Approach

  1. State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
  2. Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
  3. Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
  4. Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.

Follow-up

  • What threshold on that alert would have proved you right, and did anyone ever look at it?
  • If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?

Ship under a deadline and bound the debt you chose

Medium · pagination · technical debt · tradeoffs

You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.

Approach

  1. Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
  2. Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
  3. Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
  4. Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.

Follow-up

  • At what page depth does the offset version breach your latency budget, given your page size and row counts?
  • What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?

Unblock an engineer without taking the keyboard

Easy · mentoring · leases · at-least-once

A teammate has spent two days on a job handler that occasionally writes duplicate rows. They are certain the queue is delivering twice by mistake. You suspect a lease expiring under a slow handler, so the job is running concurrently with itself. Describe how you have unblocked someone in this position: what you asked before offering a hypothesis, what you showed them rather than told them, and what you left them owning. Then say what you would do if their theory turned out to be the right one.

Approach

  1. Ask before diagnosing, and ask for things answerable from data they already have: the attempt count on the job rows that produced duplicates, the handler's observed duration against its lease expiry, and whether the duplicate rows share a natural key that a unique constraint could have caught.
  2. Teach the shape rather than the answer. A lease cannot distinguish a dead worker from a slow one, so a handler that outruns its lease is running twice by design, and deploys deliver the other half by killing handlers mid-run on every rollout. Both of their candidate theories produce identical duplicate rows, which is why the evidence has to come from timings rather than from argument.
  3. Hand over a checklist they execute: a natural key on every write the handler performs so the second copy collides rather than appends, the record of intent written before any external effect, a lease heartbeat while running, and the metric that shows it working.
  4. Keep ownership with them deliberately. Pair on the first write, then step back; if you finish it yourself you have closed one ticket and left the same person stuck on the next redelivery.

Follow-up

  • How would you distinguish a genuine double-delivery from a lease expiry using only the data already stored?
  • Their handler calls an external endpoint before recording that it did. What do you tell them to change first?