Skip to content
QSWEQB

Java and the Spring ecosystem

Java is the default language of large enterprise back ends, and most Java jobs are really Spring jobs sitting on a JVM you are expected to understand. Demand is enormous, and much of the work is modernising estates that already exist.

Very high demand22 min readBackend engineer (Java), Senior software engineer, Java/Spring Boot developer, Integration engineer, Application architect, Site reliability engineer supporting JVM services, Android engineerUpdated 2026-07-27

Assumes you know: Programming fundamentals in any language: types, functions, control flow, Basic object-oriented ideas (class, interface, inheritance), SQL and relational data basics, Comfort with a command line and a build tool

Overview

What this area actually covers

Java is three things that people bundle into one word. There is the language — statically typed, class-based, garbage-collected, deliberately conservative about new syntax. There is the JVM, the virtual machine that executes compiled bytecode, and which does the work that makes Java fast: just-in-time compilation, garbage collection, and a memory model that defines what one thread is allowed to see of another's writes. And there is the ecosystem — Spring above all, plus Hibernate, Maven and Gradle, JUnit, Kafka clients, and the accumulated libraries of thirty years. When a job advert says Java, it almost always means the third thing standing on the first two.

The separation matters more than it sounds, because the three layers fail differently and are interviewed differently. Language questions are about semantics you can reason about from the specification: what equals must guarantee, why generics are erased, what a sealed hierarchy buys the compiler. JVM questions are about a machine whose behaviour you observe rather than read: why a pause happened, where the memory went, what the just-in-time compiler did to a loop before it started behaving oddly. Ecosystem questions are about a framework that does a great deal on your behalf and expects you to know which parts. A candidate who is strong in one layer and blank in the other two is the most common profile in this market, and the gap is usually the JVM.

The boundary that catches people out is Android. Android runs Java-language code (and increasingly Kotlin) but not on a standard JVM, and not against the standard server-side libraries. Its lifecycle, threading rules and tooling are a separate discipline with a separate hiring market. An excellent Spring engineer is not thereby an Android engineer, and the reverse is just as true. Treat them as cousins, not the same job.

Two more boundaries are worth drawing while you are here. Jakarta EE — the standardised application-server world formerly called Java EE, whose packages moved from javax to jakarta when stewardship transferred to the Eclipse Foundation — is a distinct tradition that overlaps Spring in vocabulary and diverges in practice. And Kotlin, Scala, Groovy and Clojure are separate languages sharing the same runtime, which is why "JVM engineer" and "Java engineer" are not synonyms. You can work on the JVM for years without writing Java, and you can write Java for years without ever thinking about the JVM.

The ten areas underneath

This section is divided into ten subsections, and the division is not arbitrary: it follows the layers above, from the language out to the framework and then to the practice of testing what you built. If you are deciding where to start, the short answer is core Java, then collections, then concurrency, because everything else assumes those three.

SubsectionWhat it is for
Core JavaThe semantics the compiler and the specification guarantee
Collections FrameworkThe data structures every codebase uses, and their internals
Concurrency & MultithreadingThe memory model and the tools built on top of it
JVM Internals & TuningThe machine underneath, and reading what it tells you
Streams & Functional JavaDeclarative data processing and its costs
Modern Java FeaturesWhat changed after JDK 8, and what idiomatic now means
Spring CoreThe container, and how injection and proxying really work
Spring BootAuto-configuration, properties and production readiness
Spring Data & JPAPersistence, the entity lifecycle, and generated SQL
Testing on the JVMWhere the test boundaries fall and what to mock

Core Java

Core Java is the language as the compiler and the specification define it: object identity versus equality, the equals and hashCode contract, generics and type erasure, checked and unchecked exceptions, immutability, and how parameters are passed. It exists as a separate area because these are the rules that never change underneath you, and because almost every subtler bug elsewhere turns out to be one of them misunderstood. You will find questions here that look elementary and are not: why two objects that are equals must have the same hash code, what erasure means for a List<String> at runtime, why an immutable class needs more than final fields.

Collections Framework

The collections subsection covers the implementations rather than the interfaces: how HashMap distributes keys into buckets and what it does when a bucket gets long, how ArrayList grows and what that costs, which collections guarantee iteration order and which merely appear to, and how the concurrent variants differ from the synchronised wrappers they replaced. It is separate from core Java because the interview treats it separately — collections questions are the single most reliable opener in a Java loop, and they are really questions about whether you have read the source of something you use daily. Open this one when you can say what a hash collision is but not what happens to lookup performance when they pile up.

Concurrency & Multithreading

This is where the Java memory model lives: happens-before, volatile, synchronized, the atomics, the executor framework, the java.util.concurrent data structures, and virtual threads. It is a separate area because it is the part of the platform where intuition is actively wrong. Code that looks correct on your machine is permitted by the specification to misbehave on another, and the failures are non-deterministic, load-dependent and rarely reproducible in a test. Expect questions about visibility rather than about mutual exclusion, about what an executor does when its queue fills, and about deadlock diagnosis from a thread dump.

JVM Internals & Tuning

Class loading and the delegation model, the memory regions, the garbage collectors and how they differ, just-in-time compilation, and the practical skill of reading a garbage-collection log or a heap dump. This exists as its own area because it is the layer that decides whether you can operate what you wrote. It is also the clearest seniority signal in the whole section: the ability to take a service that pauses under load and narrow it to a collector choice, a heap sizing mistake, a container memory limit or a leak is not something you can acquire from syntax practice. What you will find here is less API and more diagnosis.

Streams & Functional Java

Lambdas, method references, the Stream pipeline, collectors, Optional, and parallel streams. Streams get their own subsection because they introduce a different evaluation model — lazy, pipelined, terminal-operation-driven — into an otherwise eager language, and the traps follow from that model rather than from the syntax. The interesting questions are about when a stream is genuinely clearer than a loop, why a parallel stream can be slower, and what the common fork-join pool has to do with your unrelated request handler suddenly slowing down.

Modern Java Features

Records, sealed types, pattern matching for switch and instanceof, text blocks, var, and the wider shift in what idiomatic Java looks like since JDK 8. It is separate because it is genuinely a different dialect, and because the gap between what the language offers and what a given codebase uses is one of the defining awkwardnesses of Java work. Reading this area tells you what a modern codebase looks like; it does not excuse you from reading an old one. Interviewers use it to find out whether you have kept up, and occasionally to find out whether you know when a record is the wrong choice.

Spring Core

Dependency injection, the bean lifecycle, scopes, configuration classes, the application context, and above all proxies and aspect-oriented programming. This is the mechanism subsection: it explains how the framework does what it does, and therefore why an annotation sometimes silently has no effect. If you have ever wondered why @Transactional did nothing on a self-invoked method, or why a prototype-scoped bean injected into a singleton stayed the same object, the answer is here rather than in Spring Boot.

Spring Boot

Auto-configuration and how conditional beans are selected, the property source hierarchy and its precedence rules, profiles, the actuator endpoints, starters, and what production readiness means in this stack — health checks, metrics, graceful shutdown, container-aware memory settings. It is separate from Spring Core because Boot is a set of conventions layered on the container rather than a new container, and confusing the two is common. Open this when you can wire a bean by hand and want to know what decided to wire two hundred of them for you.

Spring Data & JPA

The entity lifecycle, the persistence context, lazy loading, the N+1 select problem, transaction propagation, fetch strategies, and the recurring discipline of looking at the SQL your code generated. Persistence gets its own area because it is where most production performance problems in Spring applications actually originate, and because the abstraction is unusually leaky by design: JPA hides the database until it cannot, and the skill is knowing the shape of what is underneath. Nearly every question here is answerable by someone who habitually turns on SQL logging and unanswerable by someone who does not.

Testing on the JVM

Unit versus integration boundaries, JUnit 5 mechanics, mocking strategy and its limits, slice tests versus a full application context, and Testcontainers for running a real database in a test. This is a separate area because testing decisions in a Spring codebase are architectural decisions in disguise: what you choose to mock determines what your tests can prove, and a suite of two thousand tests that all stub the repository proves very little about a persistence bug. Expect questions about the trade-off rather than about the annotations.

Where it sits in a real system

Java sits in the middle. In front of it is usually something else: a browser application in TypeScript, a mobile client, an API gateway. Behind it is a relational database, a message broker, a cache, and other services. The Java process is where a request becomes a decision — validate this payment, price this policy, reserve this stock — and where that decision becomes durable by being written to a database inside a transaction. Concretely: an HTTP request maps to a controller method, business logic runs, JPA or plain SQL talks to Postgres or Oracle, an event goes to Kafka, JSON comes back. Spring Boot packages that as a single executable JAR with an embedded web server, running in a container on Kubernetes. That shape describes an enormous fraction of the world's commercial back ends.

flowchart TD
    A[HTTP request] --> B[Servlet container thread]
    B --> C[Filter chain and security]
    C --> D[Controller method]
    D --> E[Transactional service proxy]
    E --> F[Repository and JPA]
    F --> G[Database]
    E --> H[Event published to broker]

The edge worth noticing is between the controller and the service: the transaction begins at the proxy, not at the controller, so anything the controller does before that call is outside it and anything it does after is already committed.

The industries matter, because they explain the language's persistence. Banking cores, card processing, insurance policy administration, telecom billing, airline and logistics operations, large retail order management, and government tax and benefits systems are disproportionately Java. These are systems where being twenty years old is a feature, where correctness beats novelty, and where a rewrite costs more than any plausible benefit. That is the installed base you are being hired into.

The JVM in four mechanisms

You do not need to know how the virtual machine is implemented, but you cannot operate a Java service without holding four mechanisms in your head. Everything in the tuning subsection is a consequence of one of them.

Class loading. Classes are loaded lazily, by loaders arranged in a delegation hierarchy: a loader asks its parent before attempting to load a class itself. That single rule explains why a library bundled in your application cannot override a platform class, why the same class name loaded by two different loaders produces two incompatible types, and why the phrase "jar hell" exists. Application servers and plugin systems build isolation out of this deliberately; the rest of us meet it as a NoSuchMethodError at runtime on code that compiled cleanly.

Memory regions. The heap holds objects and is where garbage collection happens. It is conventionally divided into a young generation, where new objects are allocated and most of them die almost immediately, and an old generation for the survivors. Outside the heap sit metaspace, which holds class metadata and grows in native memory rather than in the heap, one stack per thread, the just-in-time compiler's code cache, and direct byte buffers used by network and file libraries. The practical consequence is that "my container was killed for exceeding its memory limit" and "my heap is full" are different problems with different fixes, and telling them apart is the first move.

Garbage collection. Collectors trade throughput, pause time and footprint against one another, and choosing among them is a genuine architectural decision rather than a default to accept blindly.

CollectorOptimises forTypical fit
SerialSmall footprint, single coreTiny containers, short-lived tools
ParallelRaw throughputBatch jobs where pauses do not matter
G1Balanced, pause-time targetThe general default for services
ZGCVery low pause timesLarge heaps with latency requirements
ShenandoahVery low pause timesLarge heaps, alternative to ZGC

G1 has been the default collector since JDK 9. ZGC became production-ready in JDK 15, gained a generational mode in JDK 21, and that generational mode became ZGC's default behaviour in JDK 23. If a role talks about latency-sensitive services with large heaps, expect this table to become a conversation.

Just-in-time compilation. Bytecode starts interpreted and is compiled to native code once the JVM decides a method is hot, through tiered compilation using two compilers with different aggressiveness. The compiler makes speculative assumptions — this branch is never taken, this call site always resolves to one implementation — and deoptimises when reality disagrees. This is why a Java benchmark that does not warm up measures nothing, why performance can change after several minutes of production traffic, and why a bug can appear only under load: the code executing then is not the code that ran during your test.

Who does this work

The core role is backend engineer, and a real day involves less writing of new classes than you would guess. It is reading an existing service to find where a rule lives, adding a field that has to travel through five layers, working out why an integration test is flaky, and reviewing someone else's pull request. Interviews skew heavily towards Spring configuration, collections, concurrency and JPA because that is what the work touches.

Around that role sit distinct populations. Application architects decide service boundaries and which internal framework a new service should use. Platform and SRE teams own the runtime: heap dumps, garbage-collection logs, container memory limits, and the page when a service pauses. Integration engineers connect Java systems to brokers, file feeds and third-party APIs, which in older estates is most of the work. Modernisation teams move applications off an application server or from JDK 8 to a current LTS release, which is a specialised skill in its own right.

RoleUnit of workWhat they are graded on
Backend engineerA merged change in one serviceCorrectness, review quality, delivery
Integration engineerA working interface between systemsResilience, contract stability
Application architectA decision and its recordBoundaries that hold up over years
Platform / SREThe runtime and its behaviourAvailability, latency, cost
Modernisation engineerA migrated applicationNo regressions, no outage

The split to watch in interviews is between the people who build the service and the people who are paged for it. In many organisations those are the same people, and the questions then cover both. Where they are separate, an application team can go years without opening a garbage-collection log, which is exactly the gap a senior interview probes.

Demand, adoption and how that is changing

Demand is very high, and the reason is unglamorous: the installed base. Enormous quantities of revenue-carrying software are written in Java, they cannot be switched off, and every one of them needs people who can change it safely. That floor does not depend on Java being fashionable, which is why Java hiring is comparatively stable through cycles that hit newer stacks harder.

Three forces shape the work. The LTS cadence: since JDK 9 Java has shipped every six months with long-term-support releases every few years — 8, 11, 17, 21, 25 — and large organisations move between them in multi-year programmes, so the gap between what the language can do and what a given codebase does is often a decade wide. Containers and the cloud made JVM memory behaviour everyone's problem rather than the operations team's, and made startup time and image size matter in a way they never did on a long-lived application server; that pressure produced GraalVM native images and frameworks like Quarkus, which have real adoption without displacing Spring. Kotlin has taken a meaningful share of new JVM work while running on the same JVM and calling the same libraries, so it competes with the language more than with the platform.

The migration story deserves its own paragraph, because a large share of advertised Java roles are some part of it. Moving from JDK 8 is not a recompilation. The module system arrived in JDK 9 and progressively closed off reflective access to internal classes that libraries had been reaching into for years, so the first build after the jump fails in unfamiliar ways. The javax to jakarta package rename means every import in a Jakarta EE dependency chain changes at once, which is why organisations schedule it as a programme rather than a ticket. Tooling, agents, bytecode-manipulating libraries and application servers all have to move together. None of this is intellectually hard; all of it is slow, and the people who have done it once are disproportionately valuable because they know the order to do it in.

Be honest about the shape of the demand: a large share of Java roles are maintenance and modernisation rather than greenfield. That is not a criticism and not a lesser job. Changing a system thousands of people depend on, with no window in which it may be broken, is harder than starting from an empty directory, and it is paid accordingly. What it means is that the skills rewarded are reading comprehension, incremental refactoring and debugging under constraint, not the ability to scaffold a new project quickly.

What makes it hard

The genuine difficulty is that Java has two layers and most people learn only one. You can be productive for years knowing Spring annotations without knowing what the JVM does with your objects. That works until production, at which point the questions stop being about syntax: why does this service pause for two seconds under load, why does memory climb until the container is killed, why is this counter wrong only on the busiest day.

Concurrency bites hardest, because Java's memory model permits things that look impossible. Without synchronisation, one thread's write is not guaranteed to be visible to another, ever — the compiler, the JVM and the CPU may all reorder or cache. The classic demonstration:

class Worker {
    private boolean running = true;          // no volatile: no visibility guarantee
    // private volatile boolean running = true;  // this line is the whole fix

    void run() {
        while (running) { /* hot loop, no synchronisation */ }
    }

    void stop() { running = false; }         // may never be observed by run()
}

That loop can spin forever after stop() returns, on a correct JVM, because nothing establishes a happens-before relationship between the write and the read. The fix is one keyword. Finding it in a system you did not write, from a symptom that only appears once the JIT compiler has optimised the loop, is what experience buys.

Virtual threads, standard since JDK 21, change the arithmetic of concurrency without changing this rule. A virtual thread is scheduled by the JVM onto a small pool of platform threads and parks rather than blocking one when it waits, so code written in a plain blocking style scales to very large numbers of concurrent operations. What that does not do is make unsynchronised shared mutable state safe. It also shifts where the bottleneck sits: with threads no longer scarce, the connection pool, the downstream service and the database become the limit, and a naive migration can turn a queue that used to form politely in a thread pool into a flood arriving at a database that cannot absorb it.

The second hard thing is Spring's indirection: dependency injection, proxies and auto-configuration mean the code you read is not the code that runs. @Transactional works by wrapping your bean in a proxy, so a method calling another @Transactional method on the same object bypasses the proxy and gets no new transaction — invisible in the source, obvious once you know the mechanism. Engineers who cannot explain how the framework does its work are stuck whenever it does something they did not expect, which in a large estate is weekly.

The third is persistence, and it is the one that costs the most money. JPA manages entities through a lifecycle, and almost every surprising behaviour follows from which state an object is in and when the persistence context is flushed.

stateDiagram-v2
    [*] --> Transient
    Transient --> Managed: persist
    Managed --> Detached: context closes
    Detached --> Managed: merge
    Managed --> Removed: remove
    Removed --> [*]: flush and commit

The transition to look at is Managed to Detached: an entity that leaves the persistence context stops tracking changes, so a setter that worked inside a transaction silently does nothing outside one, and touching a lazy association there throws instead of loading.

The fourth is not technical at all. Large Java codebases carry layers of abstraction built by people who are no longer there, for reasons that were sound at the time and are undocumented now. Working effectively means developing a tolerance for reading code you did not choose, resisting the urge to rewrite what you have not yet understood, and making the smallest change that is defensible. That is a temperament as much as a skill, and it is genuinely what distinguishes people who thrive in this ecosystem from people who find it exhausting.

Why study it

Study Java if you want to work on systems where the stakes are real and the code outlives the team: finance, insurance, telecom, logistics, public sector, large-scale commerce. The demand is broad and geographically distributed rather than concentrated in a few tech hubs, the profiling and debugging tooling is the best of any mainstream runtime, and what you learn about garbage collection, memory models and JIT compilation transfers to any managed language later.

There is a second, less obvious argument. Java is unusually well documented at the specification level, which means questions about it have answers rather than opinions. When you want to know what is guaranteed about visibility between two threads, there is a document that says so, and it is readable. Very few ecosystems give you that, and learning to answer a question by consulting a specification rather than a forum post is a habit that pays out everywhere else.

Do not study it because you want the shortest path to a first startup job — that is usually TypeScript or Python. Do not study it as a route into data science or machine learning, where Python owns the ecosystem outright. And if your goal is mobile, go to Android and Kotlin directly; the overlap with server-side Java is smaller than the shared syntax suggests. If verbosity genuinely bothers you at a level you cannot argue yourself out of, be honest with yourself: modern Java is much less ceremonious than its reputation, but plenty of the code you will be paid to maintain was written before any of that arrived.

Your first hour

Do not start with a tutorial that generates a project for you. Install a current LTS JDK and write one file by hand:

java --version                    # confirm the JDK, note the version number
java Puzzle.java                  # single-file source launch, no build tool needed

In Puzzle.java, write a main that does four things and prints each result: put two distinct String objects with the same characters into a HashMap as keys and show you get one entry; compare those two strings with == and with equals and show they disagree; create a class with equals overridden but not hashCode, use it as a HashMap key, and show the lookup failing; then define a record for the same data and show the lookup working, because the record generated both methods for you. Finally run it with java -Xlog:gc Puzzle.java and read the garbage-collection lines. You will not understand all of them — note down two questions the output raises.

If you have a second hour, spend it on the framework rather than the language. Generate a minimal Spring Boot application, add one endpoint, and then do the one thing tutorials never ask you to do: turn on the debug logging for auto-configuration and read the report of which configurations were applied and which were skipped and why. It is a long piece of output and most of it will mean nothing yet. The point is to see, once, that the framework made several hundred decisions on your behalf and left a record of each one. That single observation reframes every later Spring question from "which annotation do I need" to "what already decided this, and how do I override it".

The artefact is one file, four printed results you can explain, and two questions about the JVM. That is a better foundation than a working CRUD application you cannot account for, and the hashCode result alone answers a question you will be asked for the rest of your career.

What this is not

Java is not slow, and repeating that dates you by fifteen years — long-running JIT-compiled JVM code is competitive with anything short of carefully written native code, and the real cost is memory footprint and startup time, not throughput. Java is not JavaScript. Jakarta EE application servers are a distinct older world that Spring Boot largely displaced for new work while remaining alive in existing estates. And Spring is not Java: an engineer who knows only annotations has learned a product, not a platform.

Modern Java is also not the Java of its reputation. Records give you value-like data classes without ceremony, sealed types let you close a hierarchy so the compiler can check a pattern match is exhaustive, var removes repeated type names on locals, and virtual threads (standard since JDK 21) let you write blocking code that scales like asynchronous code, because a blocked virtual thread parks instead of holding an operating-system thread. But knowing those features is not the same as working in a codebase that uses them. Plenty of teams are on an older LTS release for entirely defensible reasons, and arriving with only the modern idioms and no ability to read a decade-old AbstractServiceFactory is its own kind of unpreparedness.

Garbage collection is not the absence of memory management. It is the absence of manual deallocation, which is a much narrower claim. You can still leak comprehensively in Java, and the usual mechanism is a collection that grows forever — a cache with no eviction, a listener registry nobody unregisters from, a static map keyed by something unbounded. The collector cannot free what is still reachable, and reachability is your design decision, not its.

Finally, an ORM is not a way to avoid knowing SQL. JPA and Hibernate generate SQL, and every performance conversation about a Spring application eventually becomes a conversation about the statements it produced and the round trips it made. The engineers who are effective here are the ones who treat the generated SQL as something to be read routinely rather than as an implementation detail they are entitled to ignore.

The line between a Spring user and a Java engineer is whether you can explain what the framework and the JVM are doing on your behalf when neither behaves as you expected.

Where to go next

Now practise it

15 interview questions in Java & Spring, each with the rubric the interviewer is scoring against.

All Java questions
javajvmspringbackendconcurrency