Transactions • JPA Internals • Proxies • Production • Leadership • 2026

Spring Boot Interview Questions for 10+ Years Experience (Senior)

Spring Boot interviews for 10+ years of experience go after what only production teaches: transactions that roll back when nobody asked, writes that never reach the database, Hibernate reordering or multiplying your SQL, proxies that quietly vanish and pods killed while the heap looks fine. Then they move to design and people: multi-tenancy, pulling a service out of a monolith, leading an incident, keeping many teams current and a project that failed. It is written for Spring Boot engineers with around eight to fifteen years of experience, interviewing for senior, staff or lead roles. Each question shows what the interviewer is checking, a shape for your answer and a sample to adapt with a story from your own work.

Search all questions by round, difficulty and level, or save the ones you want to practice.

Transactions 3 questions

Hard Technical round Senior Practice question

1. A service calls another bean's @Transactional method, catches the exception it throws and carries on. At the end, the commit fails with UnexpectedRollbackException. Why, and what are the right fixes?

What the interviewer is really testing:
Whether you know a joining method marks the whole shared transaction rollback-only, and that catching the exception afterwards can't undo that.
Answer frame:

One transaction: with REQUIRED the inner method joins the outer transaction, so there is only one real transaction.

Rollback-only: when the exception leaves the inner bean's proxy, Spring marks that shared transaction rollback-only; catching it later changes nothing.

Fixes: check first instead of relying on the exception, use noRollbackFor for an expected business exception, or give truly independent work its own transaction.

Sample spoken answer:

“With the default REQUIRED propagation, the inner method doesn't start a transaction, it joins the outer one. So there's only one real transaction. When the runtime exception passes back out through the inner bean's proxy, Spring can't roll back just that part, so it marks the whole shared transaction as rollback-only. The outer method catches the exception and carries on, but at commit the transaction manager sees the flag, rolls everything back and throws UnexpectedRollbackException, so the rollback isn't silent. The fix depends on intent. If the failure is expected, like a bad row in an import, I'd check for it before calling, or mark that exception with noRollbackFor. If the inner work really is independent, it gets REQUIRES_NEW, knowing that costs a second connection. With JPA I'm extra careful, because after a persistence exception the session itself may be in a bad state, so carrying on in it is risky anyway.”

Code:
@Transactional
public void importAll(List<Row> rows) {
    for (Row row : rows) {
        try {
            rowService.importRow(row); // @Transactional, joins this transaction
        } catch (InvalidRowException e) {
            log.warn("skipped row {}", row.id()); // too late: already rollback-only
        }
    }
}
Red flag to avoid:

Saying the catch block prevents the rollback, or fixing it by making every method REQUIRES_NEW without thinking about connections.

They may ask next:
  • Would the same thing happen if the inner method were in the same class and called directly?
  • When would you rather let the whole import fail than skip the bad rows?
Say it in 60 seconds
Hard Technical round Senior Practice question

2. An @TransactionalEventListener in the after-commit phase saves an audit row through a repository. There's no error, but the row never appears in the database. What's going on?

What the interviewer is really testing:
Whether you know the after-commit callback still runs with the finished transaction's resources bound, so a write that joins it is never committed.
Answer frame:

Timing: after commit, the original transaction is done, but its connection and persistence context are still bound to the thread.

The trap: a repository call with REQUIRED joins that finished transaction; the save lands in the persistence context and no commit ever follows.

Fix: run the listener's write with REQUIRES_NEW, or hand the work to another thread or a queue.

Sample spoken answer:

“The listener runs after the commit, but before Spring has cleaned up the transaction's resources. The connection and the persistence context are still bound to the thread. So when the listener calls the repository, the default REQUIRED propagation sees an existing transaction and joins it. With JPA, save just puts the entity in the persistence context, and since that transaction has already committed, there's no flush and no commit coming. The row is quietly dropped. Spring's own docs warn about this. The fix is to run the listener's write in its own transaction with REQUIRES_NEW, or to hand the work to an async executor or a queue so it runs on a clean thread. I'd also remember that if the event is published with no transaction active, an after-commit listener doesn't run at all unless fallbackExecution is set, which surprises people in batch code.”

Code:
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void onOrderPlaced(OrderPlaced event) {
    auditRepository.save(AuditEntry.of(event));
}
Red flag to avoid:

Assuming the listener gets a fresh transaction of its own, or blaming the repository instead of the propagation.

They may ask next:
  • If the listener throws an exception, does the original transaction roll back?
  • Why is an after-commit listener still not enough when the side effect must never be lost?
Say it in 60 seconds
Hard Technical round Senior Practice question

3. Under load, every request hangs until it times out waiting for a database connection, yet the database itself is idle. The code uses REQUIRES_NEW for audit logging. How can that deadlock the pool?

What the interviewer is really testing:
Whether you can see that each REQUIRES_NEW needs a second connection while the first is still held, and how that starves a fixed-size pool.
Answer frame:

Two connections per request: the outer transaction holds one; REQUIRES_NEW suspends it and borrows another.

Starvation: when every pool slot is held by an outer transaction, all threads wait for a second connection that never frees up.

Fix: keep nested new transactions off the hot path, write the audit after commit or through a queue, or size the pool above the threads that can nest.

Sample spoken answer:

“REQUIRES_NEW suspends the current transaction, but it doesn't give that connection back. It borrows a second one from the pool. So each request needs two connections at the same moment. Picture a pool of ten and ten busy request threads: all ten take a connection for the outer transaction, then all ten ask for a second one for the audit. Nothing is free, nobody can finish, and they all wait until the pool's timeout fires. The database looks idle because nobody is actually querying. A thread dump shows every worker parked inside the pool's getConnection. To fix it, I'd get the nested transaction off the hot path: write the audit row in the outer transaction if it should roll back with it, or after commit or through a queue if it shouldn't. If nesting really has to stay, the pool must be larger than the number of threads that can nest, and I'd cap request concurrency to match.”

Red flag to avoid:

Only raising the pool size without explaining why the requests are waiting on each other.

They may ask next:
  • What exactly would you look for in the thread dump?
  • How would you catch this kind of problem in a load test before production?
Say it in 60 seconds

Bean Internals 2 questions

Hard Technical round Senior Practice question

4. @Transactional and @Async stop working on one service after someone injects it into a custom BeanPostProcessor. The startup log says the bean is not eligible for all BeanPostProcessors. What happened?

What the interviewer is really testing:
Whether you understand that BeanPostProcessors are created early, and anything they depend on is built before the proxy-creating processors are ready.
Answer frame:

Order: BeanPostProcessors are created before normal beans, so their dependencies are created early too.

Effect: those early beans miss later processors, including the one that wraps beans in transaction and async proxies.

Fix: keep processors free of business beans, declare their @Bean methods static, and look up dependencies lazily with ObjectProvider.

Sample spoken answer:

“Spring creates all BeanPostProcessors before the regular beans, because they have to be ready to process them. If a processor has a dependency, say an OrderService, then OrderService gets created during that early phase, before every processor is registered. The processor that builds the proxies for @Transactional and @Async may not be active yet, so OrderService ends up as a plain object with no proxy. The annotations are still in the code, but nothing acts on them. Spring does log a line saying the bean is not eligible for getting processed by all BeanPostProcessors, and most people scroll straight past it. The fix is to keep processors lean: no business beans injected, @Bean methods for processors declared static so they don't drag their configuration class in early, and if a processor truly needs another bean, it fetches it lazily through ObjectProvider at the moment it's used.”

Red flag to avoid:

Blaming self-invocation without noticing the log line or the processor's dependency.

They may ask next:
  • Why should a @Bean method that returns a BeanFactoryPostProcessor be static?
  • How would you write a test that fails if a service loses its transactional proxy?
Say it in 60 seconds
Hard Technical round Senior Practice question

5. A @Transactional service method throws a NullPointerException on an injected repository, but only that one method. The method is final, or the service is written in Kotlin. What's happening?

What the interviewer is really testing:
Whether you know how a class-based proxy is built, and why a final method runs on the proxy object itself, whose fields were never set.
Answer frame:

How the proxy works: Boot proxies beans by subclassing them, and each overridden method forwards to the real bean.

Final methods: can't be overridden, so the call runs on the proxy instance, whose fields were never injected, and no transaction starts.

Fix: no final on proxied methods; in Kotlin, the Spring compiler plugin opens annotated classes and their members.

Sample spoken answer:

“Spring Boot proxies beans by creating a subclass at runtime. Each method the proxy can override runs the transaction logic and then calls the real bean, which has all its dependencies. A final method can't be overridden, so when someone calls it on the proxy, the original method body runs on the proxy object itself. That proxy was created without running your constructor or any injection, so its fields are null, and the first use of the repository throws. There's no transaction either, so even without the crash the annotation would do nothing. Kotlin makes this easy to hit, because classes and methods are final by default. The kotlin-spring compiler plugin opens classes carrying Spring annotations like @Component and @Transactional, and a module that forgot to apply it brings the bug back. Spring can log a note about final methods, but it's easy to miss.”

Code:
@Service
public class InvoiceService {
    private final InvoiceRepository repo;

    public InvoiceService(InvoiceRepository repo) { this.repo = repo; }

    @Transactional
    public final void close(long id) { // final: runs on the proxy, repo is null
        repo.findById(id).ifPresent(Invoice::close);
    }
}
Red flag to avoid:

Blaming the repository bean or the database, without looking at how the proxy calls the method.

They may ask next:
  • Why does a private @Transactional method fail quietly instead of throwing?
  • How would you write a test that catches this before production?
Say it in 60 seconds

JPA Internals 4 questions

Hard Technical round Senior Practice question

6. Queries like findByIdIn get lists of every size from one to a thousand ids. Memory creeps up and the database's statement cache churns. What's the link, and how do you fix it?

What the interviewer is really testing:
Whether you know each list size produces a different SQL statement, and how to keep the number of statement shapes small.
Answer frame:

Cause: an IN list binds one parameter per value, so every list size is a different SQL string with its own cache entries.

Padding: Hibernate's in-clause parameter padding rounds the count up to a power of two, so a few shapes cover every size.

Big lists: split them into chunks, or use an array parameter or a temporary table where the database supports it.

Sample spoken answer:

“An IN list with one bound parameter per value means a list of three ids and a list of four ids are two different SQL statements. Hibernate caches a query plan per statement, and the driver and the database cache prepared statements per statement, so with a thousand possible sizes you get up to a thousand near-identical entries in each. The caches are bounded, but they fill with junk and push out the plans you actually reuse, and the database keeps parsing new statements. The quick fix is Hibernate's in-clause parameter padding setting. It rounds the parameter count up to the next power of two and repeats the last value, so a handful of shapes cover every list size. For really big lists I'd split them into chunks, since some databases cap the number of items in an IN list, or pass an array parameter or join against a temporary table where the database supports it.”

Code:
spring:
  jpa:
    properties:
      hibernate.query.in_clause_parameter_padding: true
Red flag to avoid:

Blaming a leak in Hibernate without noticing that every list size creates a new statement.

They may ask next:
  • Why doesn't padding a list of five ids up to eight change the query's result?
  • How would you spot this problem from the database's own statistics?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

7. A paged endpoint uses a fetch join to load orders with their items. It works in testing, but in production each page takes seconds and memory spikes. What is Hibernate doing?

What the interviewer is really testing:
Whether you know SQL paging can't be applied to a collection fetch join, so Hibernate loads every row and pages in memory.
Answer frame:

Cause: a join to a collection repeats parent rows, so Hibernate can't use SQL limit and offset; it loads everything and pages in memory, with a warning in the log.

Why tests missed it: with small test data, loading everything looks instant.

Fix: page the parent ids first, then fetch those parents with their items, or use batch fetching instead of the join.

Sample spoken answer:

“When you fetch join a collection, each order row repeats once per item. A limit of twenty rows might cover only three orders, so Hibernate can't push the page down to SQL. Instead it drops the limit, loads every matching row, builds all the orders and cuts the page out in memory. It logs a warning about applying pagination in memory, but that's easy to miss. With test data it's instant. With a production table it loads everything on every page request. The fix I use is two queries: first select one page of order ids with proper SQL paging and a stable order, then fetch those orders with their items using a fetch join on an IN clause. Another option is to drop the join and set a batch fetch size, so the items load in a few batched queries per page. Either way, I'd add a test that checks the query count or fails on that warning.”

Red flag to avoid:

Blaming the database or adding an index without noticing the paging happens in memory.

They may ask next:
  • Why does the count query behind a Page also need care here?
  • Would an entity graph avoid the problem?
Say it in 60 seconds
Hard Technical round Mid-level, Senior Practice question

8. Fetching an order with both its items and its payments in one query throws MultipleBagFetchException. Changing both Lists to Sets makes the error go away, but the query gets slow. Why?

What the interviewer is really testing:
Whether you see the Cartesian product behind two collection fetch joins, and know the Set change hides the error rather than fixing it.
Answer frame:

Why Hibernate refuses: two List collections joined in one query multiply the rows, and Hibernate can't tell real duplicates in a bag from join duplicates.

Why Set is worse than it looks: the error goes away, but the query still returns items times payments rows for every order.

Fix: fetch one collection per query for the same orders, or use batch fetching for the second collection.

Sample spoken answer:

“Joining two collections in one query gives a Cartesian product: an order with twenty items and ten payments comes back as two hundred rows. A List without an order column is a bag in Hibernate, and a bag is allowed to hold duplicates, so Hibernate can't tell which repeated rows are real and which come from the join. That's why it refuses to fetch two bags at once. Switching to Sets removes that ambiguity, so the error goes away, but the database still sends back every combination, and with real data that's a lot of rows to transfer and throw away. The fix I use is one collection per query. First load the orders with their items, then run a second query that fetches the same orders with their payments. Hibernate merges both into the same entities in the persistence context. Batch fetching for the second collection works too.”

Red flag to avoid:

Calling the switch to Sets a fix without noticing the row count it produces.

They may ask next:
  • Why do the payments from the second query end up on the same order objects?
  • Would an entity graph that names both collections behave any differently?
Say it in 60 seconds
Hard Technical round Senior Practice question

9. In one transaction the code deletes a user's old default address and saves a new one with the same unique key. It fails with a unique constraint violation, though the delete comes first in the code. Why?

What the interviewer is really testing:
Whether you know Hibernate reorders SQL at flush time and runs inserts before deletes.
Answer frame:

Flush order: Hibernate doesn't run SQL in the order you call it; at flush it runs inserts first and deletes last.

Result: the new row is inserted while the old one still exists, so the unique constraint fails.

Fix: update the existing row instead, or flush right after the delete so it reaches the database first.

Sample spoken answer:

“Hibernate doesn't send SQL when you call delete or save. It queues the changes and, at flush time, runs them in a fixed order by type: inserts first, then updates, then collection changes, and deletes last. That order avoids a lot of foreign key problems, but here it bites. The insert for the new address runs while the old row is still there, so the unique constraint fails, even though the code deleted first. The cleanest fix is often not to delete at all: update the existing row with the new values, which is also one statement instead of two. If it really has to be a delete and an insert, I call flush right after the delete so it reaches the database before the insert is queued. I'd leave a comment there, because the next person will think that flush is pointless and remove it.”

Code:
addressRepository.delete(oldDefault);
addressRepository.flush(); // send the delete before the insert is queued
addressRepository.save(newDefault);
Red flag to avoid:

Assuming Hibernate runs SQL in the order the code calls it.

They may ask next:
  • Why does a derived deleteBy method sometimes run a select and then one delete per row?
  • When would you use a bulk delete query instead, and what does it skip?
Say it in 60 seconds

Auto-Configuration 2 questions

Hard Technical round Mid-level, Senior Practice question

10. After a routine dependency bump, some API clients start getting XML instead of JSON. No code changed. What happened, and how do you stop this whole class of surprise?

What the interviewer is really testing:
Whether you treat the classpath as configuration in Spring Boot and know how content negotiation picks a message converter.
Answer frame:

Cause: a new transitive dependency put Jackson's XML module on the classpath, so Boot registered an XML message converter.

Who sees it: clients whose Accept header ranks XML above a wildcard, such as browsers, now get XML.

Prevention: declare produces on the API, diff the dependency tree in CI, and compare the conditions report after upgrades.

Sample spoken answer:

“In Spring Boot the classpath is configuration. If some library pulls in Jackson's XML data format module, Boot sees it and registers an XML message converter next to the JSON one. Content negotiation then looks at each client's Accept header. Clients asking for JSON are fine, but some send a header that lists application/xml ahead of a wildcard, browsers do this for example, and they now get XML back. I'd confirm it with the dependency tree to find which library brought the module in, then exclude it, or declare produces as application/json on the API so negotiation can't pick anything else. For the whole class of problem, I'd diff the resolved dependency tree in CI on every upgrade, keep a contract test that checks response content types, and compare the auto-configuration conditions report before and after, so new auto-configured beans show up in review instead of in production.”

Red flag to avoid:

Searching the application code for a change instead of looking at the classpath.

They may ask next:
  • What other behaviour can appear just because a jar lands on the classpath?
  • How would you exclude one auto-configuration class without removing the jar?
Say it in 60 seconds
Hard Technical round Senior Practice question

11. You add a second DataSource bean for a reporting database. Now writes that should roll back together don't, and schema migrations run on only one database. Explain what Boot did.

What the interviewer is really testing:
Whether you know auto-configuration backs off or binds to the primary bean, and that one @Transactional never spans two databases.
Answer frame:

Back-off: defining your own DataSource replaces Boot's, and JPA, the default transaction manager and migrations bind to the primary one.

Transactions: @Transactional uses one transaction manager; writes through the other DataSource are outside it.

Fix: explicit configuration per database with qualified transaction managers, and no cross-database atomicity without an outbox or a saga.

Sample spoken answer:

“Most Boot auto-configuration backs off as soon as you define your own bean of that type. So once you define DataSources yourself, you own them. With two, you mark one @Primary, and everything auto-configured binds to that one: JPA, the default transaction manager and the migration tool. A @Transactional method uses that primary transaction manager. A write through a JdbcTemplate on the reporting DataSource runs on a different connection and isn't part of that transaction, so a rollback leaves it in place. The fix is to configure each database explicitly: its own properties, its own transaction manager with a qualifier, and a migration setup per database. And I'd be honest that two databases can't commit atomically without distributed transactions, which I avoid. Instead I make one of them the source of truth and move data to the other with an outbox or an idempotent sync job.”

Red flag to avoid:

Believing one @Transactional automatically covers both databases.

They may ask next:
  • How do you choose which transaction manager a @Transactional method uses?
  • Why would you rather avoid a distributed XA transaction here?
Say it in 60 seconds

Security Internals 1 question

Medium Technical round Mid-level, Senior Practice question

12. A custom token filter is annotated @Component and also added to the security filter chain. Audit logs show it running twice on some requests. Why, and how do you fix it?

What the interviewer is really testing:
Whether you know Boot registers every Filter bean with the servlet container automatically, separately from the security chain.
Answer frame:

Two registrations: Boot registers any Filter bean with the servlet container, and addFilterBefore adds it to the security chain as well.

Result: it runs once inside the security chain and once as a plain servlet filter, in an order you didn't choose.

Fix: turn off the servlet registration with a disabled FilterRegistrationBean, or don't make the filter a bean at all.

Sample spoken answer:

“Spring Boot registers every bean that implements Filter with the servlet container on its own. The security filter chain is a separate list that lives inside one servlet filter. So when the class is a @Component and you also call addFilterBefore in the security configuration, it's registered twice: once in the security chain, where you placed it, and once as a plain servlet filter, wherever Boot's default ordering puts it. It runs twice, and the second run happens at a point in the servlet chain nobody chose. Extending OncePerRequestFilter can hide the second run, but that's hiding it, not fixing it. The clean fix is to create the filter inside the security configuration without making it a bean, or keep it a bean and add a FilterRegistrationBean for it with enabled set to false. I'd also add a test that counts invocations so it doesn't creep back.”

Code:
@Bean
FilterRegistrationBean<TokenAuthFilter> tokenFilterRegistration(TokenAuthFilter filter) {
    FilterRegistrationBean<TokenAuthFilter> reg = new FilterRegistrationBean<>(filter);
    reg.setEnabled(false); // keep it only in the security chain
    return reg;
}
Red flag to avoid:

Not knowing Boot auto-registers Filter beans, so the fix turns into guessing with @Order.

They may ask next:
  • When there are several SecurityFilterChain beans, how does Spring Security pick one for a request?
  • Where in the chain should a token filter sit, and why?
Say it in 60 seconds

Testing 1 question

Medium Technical round Mid-level, Senior Practice question

13. Your CI test suite takes over half an hour, and the logs show the Spring context starting again and again. What causes that, and how would you fix it across a large codebase?

What the interviewer is really testing:
Whether you know how Spring's test context cache decides when to reuse a context, and how to keep a whole codebase inside it.
Answer frame:

The cache key: a context is reused only when the configuration is identical: classes, profiles, properties and the set of mocked beans.

Usual culprits: a different set of mock beans in each class, one-off test properties, and @DirtiesContext used as a habit.

Fix at scale: a few shared test setups, slice tests where they fit, and the cache's debug statistics to prove it worked.

Sample spoken answer:

“Spring's test framework caches application contexts and reuses one whenever a test class has exactly the same configuration. The key includes the configuration classes, active profiles, test properties and the set of beans replaced with mocks. So every class with its own combination of mock beans, or its own test property, gets a brand new context. @DirtiesContext throws a context away on purpose, and teams sprinkle it around to fix flaky tests. First I'd turn on debug logging for the context cache, which reports hits, misses and size, to see how many distinct contexts we build. Then I'd standardise: a few shared base setups with an agreed set of mocks, slice tests for the web and data layers, and @DirtiesContext only where a test really breaks shared state. One more trap: cached contexts stay alive, so many of them can each hold a connection pool, and the suite runs out of database connections.”

Red flag to avoid:

Throwing bigger CI machines or parallel runs at it without asking why the context keeps restarting.

They may ask next:
  • Why can a context cached by another test class cause a flaky failure?
  • When is @DirtiesContext really the right tool?
Say it in 60 seconds

Production Operations 2 questions

Hard Technical round Senior Practice question

14. Kubernetes keeps OOM-killing a Spring Boot pod, but the heap graphs show plenty of free heap. Where is the memory going, and how do you find out?

What the interviewer is really testing:
Whether you know a JVM process uses a lot of memory outside the heap, and how to measure it instead of guessing.
Answer frame:

Outside the heap: metaspace, thread stacks, the code cache, GC structures, direct buffers used by networking libraries, and native allocations.

Measure: compare the pod's resident memory with the heap, then use Native Memory Tracking, a thread count and direct buffer metrics.

Fix: size the heap as a share of the container limit with real headroom, cap thread pools and direct memory, and alert on non-heap growth.

Sample spoken answer:

“The container is killed on total process memory, and the heap is only part of it. The rest is metaspace for classes, a stack for every thread, the JIT's code cache, the garbage collector's own structures, direct buffers that Netty and other I/O libraries allocate, and plain native allocations. So first I'd compare the pod's resident memory with the heap's committed size. Then I'd start the JVM with Native Memory Tracking and use jcmd to get a breakdown by area. Common findings are too many threads, like a huge Tomcat pool plus several executors, or direct buffers growing under a busy HTTP client. Memory used by native libraries, or fragmentation in the C allocator, won't show up in that breakdown at all, so a gap there is a clue too. The fix is heap sizing with headroom, capped pools, a direct memory limit, and Actuator's JVM metrics on a dashboard.”

Code:
java -XX:MaxRAMPercentage=70 -XX:NativeMemoryTracking=summary -jar app.jar
jcmd 1 VM.native_memory summary
Red flag to avoid:

Raising -Xmx, which leaves even less room for everything that lives outside the heap.

They may ask next:
  • Why can the number of threads matter as much as the heap size?
  • What changes when you size the heap as a share of the container limit instead of a fixed -Xmx?
Say it in 60 seconds
Hard Situational round Senior Practice question

15. You're the incident lead. Right after a deploy, every pod of a key Spring Boot service is at full CPU and requests are timing out. The team wants to add more pods. What do you do in the first fifteen minutes?

What the interviewer is really testing:
Whether you stabilise first, protect shared systems like the database, and still keep the evidence needed to find the cause.
Answer frame:

Stabilise: the deploy is the prime suspect, so roll back first and diagnose after.

Careful with scaling: more pods can overload the database or a downstream service and turn one outage into several.

Keep evidence: take thread dumps or a short profile from one pod before it's replaced, and give one person the job of updates.

Sample spoken answer:

“The change that just went out is the most likely cause, so my first call is to roll back, not to debug live. Before anyone scales out, I'd ask what each extra pod adds to the shared systems. If the CPU is going into retries or a hot loop, more pods mean more connections and more load on the database and downstream services, and one failing service becomes three. While the rollback runs, I'd ask one engineer to take a few thread dumps a few seconds apart, or a short profile, from one bad pod, so we keep the evidence once those pods are gone. One person posts updates, so the people fixing aren't answering questions. When the old version is back and the graphs are normal, we find the cause from the dumps and the diff, and the fix goes out to a single canary pod first.”

Red flag to avoid:

Debugging live for an hour before rolling back, or scaling out without checking what it does to the database.

They may ask next:
  • What if the rollback doesn't fix it?
  • What would you change in the deploy process after this incident?
Say it in 60 seconds

Architecture 2 questions

Hard System design round Senior Practice question

16. Your Spring Boot monolith has one JPA model that every module joins across. You're asked to pull billing out into its own service. How do you plan it?

What the interviewer is really testing:
Whether you know the hard part is data ownership, not the code, and can move step by step without a big-bang cutover.
Answer frame:

Boundaries first: inside the monolith, stop cross-module joins and entity references; other modules call billing through an interface.

Own the data: billing tables get one owner; other modules keep ids and read through an API or events, not joins.

Move gradually: route traffic behind a flag, run both paths with reconciliation, and keep the rollback path until the numbers match.

Sample spoken answer:

“The code is the easy part. The real coupling is the shared JPA model, where an Order entity has a direct relation to Invoice and reports join across everything. So I'd start inside the monolith. Billing becomes a module with a public interface, other modules stop holding references to its entities and keep ids instead, and I add architecture tests that fail when someone crosses the line. Queries that joined across modules get rewritten to go through that interface. Once billing's tables are only touched by billing code, pulling it out is mostly moving the module and replacing in-process calls with an API or events. For data other services need to read, billing publishes events through an outbox. I'd put the new service behind a flag, run old and new side by side with reconciliation for a while, and keep the rollback path until the numbers match.”

Red flag to avoid:

Starting with a new service on the same shared database, which keeps all the coupling and adds network calls.

They may ask next:
  • How would you handle reports that used to join billing data with everything else?
  • What would make you decide not to extract it after all?
Say it in 60 seconds
Hard System design round Senior Practice question

17. You're designing a multi-tenant SaaS product on Spring Boot. How do you choose between a database per tenant, a schema per tenant and shared tables, and how does the app keep tenants apart?

What the interviewer is really testing:
Whether you can weigh isolation, cost and operations for each model, and enforce the tenant boundary in one place instead of trusting every query.
Answer frame:

Pick the model: shared tables are cheapest; a schema or database per tenant isolates better but multiplies migrations and connections.

Resolve once: take the tenant from the verified token at the edge, keep it in a request context, and clear it when the request ends.

Enforce centrally: a tenant filter in the data layer or row-level security in the database, tenant-aware caches, and the tenant passed explicitly to async work.

Sample spoken answer:

“I'd pick the model from the customers and their rules. Shared tables with a tenant column are cheapest and simplest to run, but one missed filter leaks data. A schema or database per tenant isolates better and makes a single tenant's restore easy, but every migration runs many times and connection pools multiply. Many products mix them: shared tables by default, a dedicated database for the few large customers who need it. In the app, the tenant is resolved once, from the verified token, never from a header anyone can set, and kept in a request context that's cleared at the end, because threads get reused. Then isolation is enforced in one place: a tenant filter in the data layer, row-level security in the database, or a routing data source that picks the tenant's schema. Caches are keyed by tenant, async tasks get the tenant passed in, and migrations run from a job, not at every app's startup.”

Red flag to avoid:

Adding a tenant condition by hand to every query and trusting everyone to remember it.

They may ask next:
  • How would you run a schema migration across hundreds of tenant schemas safely?
  • How would you stop one very busy tenant from slowing everyone else down?
Say it in 60 seconds

Leadership 5 questions

Hard Situational round Senior Practice question

18. Leadership asks whether moving your many Spring Boot services to a lighter framework would cut cloud cost, since the apps start slowly and use a lot of memory. How do you answer?

What the interviewer is really testing:
Whether you can turn a cost question into measurements and options, and give leadership a clear recommendation with its trade-offs.
Answer frame:

Find the real cost: measure where the money goes; oversized pods and idle replicas usually matter more than startup.

Cheap wins first: right-sized containers and heaps, fewer replicas when idle, class data sharing, or native images for the few services that need fast startup.

Frame the migration: people cost, risk and time next to the saving, and a pilot before any fleet-wide decision.

Sample spoken answer:

“I'd first turn it into numbers: what we spend on these services and what drives it. Often it isn't the framework. It's containers sized far bigger than their measured use, heaps set without regard to the container limit, and replicas running all night with no traffic. Those fixes are cheap and fast. Startup time only matters where we scale to zero or scale up in a hurry, and for those few services Spring's ahead-of-time support with native images, or class data sharing, gives much of what a lighter framework would, without a rewrite. Then I'd put the migration in honest terms: months of engineering, retraining, new failure modes and libraries we'd have to replace, set against the saving. My recommendation would usually be to do the tuning now, pilot a native image on one service that scales up and down a lot, and revisit the framework only if the numbers still point there.”

Red flag to avoid:

Treating it as a framework popularity debate, or promising savings without measuring.

They may ask next:
  • What do you give up when you compile a Spring app to a native image?
  • How would you report the savings back to leadership?
Say it in 60 seconds
Medium Behavioral round Senior Practice question

19. Tell me how you got many teams to keep their Spring Boot versions current. What did you make easy, and what did you enforce?

What the interviewer is really testing:
Whether you've set a standard across teams that lasted, using tooling and incentives rather than a wiki page.
Answer frame:

The problem: services drifting versions behind, with security fixes piling up.

Make it easy: a shared BOM, automated update pull requests, and a platform team that does the first upgrade and writes the guide.

Enforce lightly: a supported-version policy with dates, a dashboard of who's behind, and a build warning before it becomes a failure.

Sample spoken answer:

“At my last company we had about forty Spring Boot services, and some were two major versions behind, which meant security fixes we couldn't take. Rules alone hadn't worked. So I made staying current the easy path. We moved everyone onto one internal BOM, set up automated pull requests for patch and minor updates, and my team did the first major upgrade on our own services and wrote down every breaking change we hit. For enforcement, we published a supported-version window with dates, put up a dashboard showing who was behind, and made the build warn for a quarter before it failed. The teams furthest behind got a pairing session, not a telling-off. Within about two quarters nearly everyone was on the current line, and later upgrades took days instead of months because each one was small.”

Red flag to avoid:

A story where the standard was only a document, or where one team did everyone's upgrades forever.

They may ask next:
  • What did you do with a team that still refused to upgrade?
  • How did you handle a service whose critical library didn't support the new version yet?
Say it in 60 seconds
Medium Behavioral round Senior Practice question

20. Tell me about an engineer who could build features in Spring Boot but got stuck whenever the framework did something unexpected. How did you help them grow?

What the interviewer is really testing:
Whether you grow people by teaching them to find answers themselves, not by fixing things for them.
Answer frame:

The gap: they knew the annotations but not what happens underneath, so every surprise became a blocker.

What you did: debugged real issues together with them driving, and taught the tools: conditions report, breakpoints in proxies, SQL logging.

The outcome: they handled these problems alone and started teaching others.

Sample spoken answer:

“I had a developer on my team who shipped features quickly but lost days whenever something odd happened, like a bean not being created or a transaction not rolling back. He'd try annotations until something worked. Instead of fixing things for him, I started pairing whenever he hit one, with him at the keyboard. I'd ask what Spring was supposed to do, and we'd check: the conditions report for missing beans, a breakpoint in the transaction interceptor to see whether the proxy was even there, SQL logging for JPA surprises. I also gave him a small real task, writing one auto-configuration for our team, which made him learn how it works underneath. After a few months he was the person others pinged about strange startup errors, and he ran a session for the team on reading the conditions report.”

Red flag to avoid:

A story where you kept solving the problems yourself and called it mentoring.

They may ask next:
  • How did you balance his learning time against delivery pressure?
  • How did you know it was working before those few months were up?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

21. Tell me about a Spring Boot initiative you led that failed or had to be rolled back. What did you get wrong?

What the interviewer is really testing:
Whether you own a real failure, can say precisely what you misjudged, and changed how you work afterwards.
Answer frame:

The bet: what you set out to do and why it looked right at the time.

Where it went wrong: the specific wrong assumption, not bad luck or other people.

What changed: the concrete practice you adopted afterwards.

Sample spoken answer:

“At my last company I led moving our checkout services onto a reactive stack, based on load test results from one service. I got two things wrong. I assumed the whole path could be non-blocking, but our payment client library was blocking, and the way we wrapped it tied up event loop threads under real traffic. And I moved three services at once instead of one. After a bad evening at peak, we rolled back to the MVC versions we'd kept deployable. I owned it in the review. What changed for me: I now insist on a production pilot of one service before any wider rollout, I list every dependency on the critical path and test each one under realistic load, and I keep the old path running until the new one has proven itself through a full business cycle.”

Red flag to avoid:

Blaming the framework or the team, or picking a failure that cost nothing.

They may ask next:
  • How did you explain the rollback to the business?
  • What would you do differently in the very first week?
Say it in 60 seconds
Medium Culture fit round Senior Practice question

22. How would you tell in an interview whether a senior candidate really understands Spring Boot or has only memorised annotations?

What the interviewer is really testing:
Whether you can assess depth fairly and know what a senior-level answer sounds like.
Answer frame:

Ask for the why: surprises where the obvious answer is wrong, like a @Transactional that does nothing.

Real debugging: a broken app or a stack trace, and watch how they find the cause.

Judgement: a design trade-off with no single right answer, and how they reason about it.

Sample spoken answer:

“Memorised answers break down when you ask why, or what happens next. So I ask about surprises: a transaction that didn't roll back, a bean that wasn't created, a job that ran twice. A strong senior talks about proxies, conditions and thread boundaries, and says how they'd prove it, not just which annotation to add. I also like a short debugging exercise: a small app that fails to start or fires too many queries, and they talk me through finding the cause. How they use the logs and the conditions report tells me a lot. Then one judgement question, like whether to split a service or adopt a new stack, where I listen for trade-offs and for questions back to me. I score against written criteria, so two interviewers mean the same thing by strong, and I don't mark anyone down for not recalling a property name they'd look up.”

Red flag to avoid:

Hiring on annotation trivia or property names, with no debugging or judgement.

They may ask next:
  • How do you keep a debugging exercise fair for someone who's nervous?
  • What would make you reject a candidate who answered every trivia question right?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

You practiced these. On the real call, ClapAssist helps with the rest.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your live interview audio and screen are never stored. Your resume and notes are saved to your account so the app fills them in on any computer. It stays out of screen share on every plan, including Free; only you can see it.

Download with 10 free minutes
Mac and Windows · Stays out of screen share · No card