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.
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.
Subsection
What it is for
Core Java
The semantics the compiler and the specification guarantee
Collections Framework
The data structures every codebase uses, and their internals
Concurrency & Multithreading
The memory model and the tools built on top of it
JVM Internals & Tuning
The machine underneath, and reading what it tells you
Streams & Functional Java
Declarative data processing and its costs
Modern Java Features
What changed after JDK 8, and what idiomatic now means
Spring Core
The container, and how injection and proxying really work
Spring Boot
Auto-configuration, properties and production readiness
Spring Data & JPA
Persistence, the entity lifecycle, and generated SQL
Testing on the JVM
Where 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.
Loading diagram…
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.
Collector
Optimises for
Typical fit
Serial
Small footprint, single core
Tiny containers, short-lived tools
Parallel
Raw throughput
Batch jobs where pauses do not matter
G1
Balanced, pause-time target
The general default for services
ZGC
Very low pause times
Large heaps with latency requirements
Shenandoah
Very low pause times
Large 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.
Role
Unit of work
What they are graded on
Backend engineer
A merged change in one service
Correctness, review quality, delivery
Integration engineer
A working interface between systems
Resilience, contract stability
Application architect
A decision and its record
Boundaries that hold up over years
Platform / SRE
The runtime and its behaviour
Availability, latency, cost
Modernisation engineer
A migrated application
No 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:
classWorker {
privatebooleanrunning=true; // no volatile: no visibility guarantee// private volatile boolean running = true; // this line is the whole fixvoidrun() {
while (running) { }
}
{ running = ; }
}
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.
Loading diagram…
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.