DevelopmentAll levels

Java

Java has been the default language of large back-end systems for thirty years, and it is still the one you find when a bank, an airline or a telecom opens its source tree. What makes Java what it is has less to do with its syntax than with the machine it runs on: bytecode instead of native code, a compiler that optimises while the program runs, and a garbage collector you tune rather than replace. This page covers the JVM, the type system, Spring, concurrency, and what virtual threads changed.

Artefact
Bytecode, not native
Memory
Collected, not freed
Releases
Every 6 months, LTS every 2 years

The machine underneath

Java does not compile to machine code. It compiles to bytecode — instructions for an abstract machine — and the JVM for your platform is what turns those into something the processor understands. That indirection is the source of the old slogan about writing once and running anywhere, and it is still the reason the same build artefact runs unchanged on a developer's laptop and on a Linux server.

Java compiles to bytecode rather than to machine code, so the artefact is identical everywhere and the JVM is the part that differs per platform. Inside, the interpreter runs every method and the JIT recompiles whatever gets hot into native code — which is why a service is slow for its first few thousand requests and fast afterwards. Objects go on a heap the runtime manages: most die young and are collected cheaply, survivors are promoted to the old generation, and collecting that is where the pauses come from.

Inside, the JVM starts by interpreting that bytecode, which is slow, while counting how often each method runs. When a method crosses a threshold the JIT compiler translates it to native code, and because it is compiling a program it can watch, it optimises using facts a static compiler never has: which branch is actually taken, which type really arrives at this call site, which loop is hot. This is why a Java service is measurably slower for its first few thousand requests and then settles — and why a benchmark that runs for two seconds tells you nothing about production.

Memory is the JVM's other job. Objects are allocated on the heap and freed by a garbage collector rather than by you, which removes the entire class of use-after-free and double-free bugs that dominate C and C++. The collector exploits one strong observation: most objects die almost immediately. So the heap is split into generations — new objects go into a young generation that is collected often and very cheaply, and the few that survive are promoted into an old generation that is collected rarely and expensively. Long pauses come from the old generation, which is why "reduce garbage" usually means "stop promoting objects that should have died young".

Two consequences shape how Java is deployed today. Startup is not instant — the JVM has to load classes, verify them and warm up — which is awkward for a function that scales to zero and irrelevant for a service that runs for weeks. And when startup does matter, GraalVM native images compile ahead of time into a real executable that starts in milliseconds, at the price of losing the JIT's runtime optimisation and requiring configuration for anything that uses reflection.

The language, and how much of it you need

Java is statically typed and compiled, so a misspelled method or a wrong argument is a build failure rather than a page that breaks for one user on Friday. The type system is nominal and class-based: everything lives in a class, inheritance is single, and interfaces provide the multiple-inheritance-of-behaviour that classes do not. That verbosity is the trade the language has always made, and modern versions have quietly removed a great deal of it.

  • Records

    A data carrier in one line — fields, constructor, accessors, equals, hashCode and toString generated and immutable by construction.

  • Sealed types

    A hierarchy that names its own subtypes, so the compiler knows the list is closed and can check a switch for completeness.

  • Pattern matching

    Test a type and bind it in one step, in instanceof and in switch — the replacement for a cast after every check.

  • Streams

    Declarative collection processing — filter, map, collect. Readable for a pipeline, worse than a loop for two operations.

  • Optional

    An explicit "may be absent" return type. Use it for returns; do not use it for fields or parameters, where it only adds noise.

  • var

    Local type inference. It shortens a declaration whose type is obvious from the right side, and hides it when it is not.

A word on Kotlin, because the question always comes up. It runs on the same JVM, calls Java libraries directly, and fixes the two complaints people have with Java: nullability is in the type system rather than in your head, and the boilerplate is gone. It is the default for Android and a reasonable choice for a new service. It is not a reason to rewrite a working Java codebase, and mixing both in one module is a build-configuration cost you should take deliberately.

Spring, and what a framework is doing here

Spring is close to universal on the server, and what it fundamentally provides is dependency injection: your classes declare what they need in their constructors, and the container supplies it. That sounds bureaucratic until you notice the consequence — a class that receives its collaborators can be given fake ones in a test, so business logic becomes testable without a database, a network or a running server. Spring Boot then adds opinionated auto-configuration and an embedded server, so an application is an executable rather than something you deploy into a container.

LayerWhat it doesWhere it bites
DI containerBuilds and wires your objects, manages their lifecycle and scope.Field injection hides dependencies; ask for them in the constructor instead.
Spring MVC / WebFluxMaps HTTP to methods. MVC is blocking per request; WebFlux is reactive.WebFlux only pays off if the whole chain is non-blocking, including the driver.
Spring Data / JPAMaps objects to tables and derives queries from method names.Lazy loading turns one call into N queries; the SQL log is the only honest witness.
TransactionsAn annotation opens and commits a transaction around a method.A call from inside the same class bypasses the proxy, so the annotation does nothing.

Threads, and what virtual threads changed

A Java thread has always been a real operating-system thread. That gives genuine parallelism across cores — unlike PHP, Node or CPython, Java can saturate every core inside one process — and it costs about a megabyte of stack each, which is why servers pool them. The classic Java web server assigns a thread per request, and its concurrency limit is the size of that pool: two hundred threads means two hundred in-flight requests, no matter that most of them are only waiting for a database.

Virtual threads, standard since Java 21, remove that ceiling. They are scheduled by the JVM onto a small number of real threads, cost kilobytes instead of megabytes, and — this is the point — when one blocks on I/O, the JVM parks it and reuses the carrier thread for something else. The result is that ordinary blocking code, written in the straightforward style everyone already knows, scales to hundreds of thousands of concurrent requests. That is why the reactive style, which bought the same scalability at the price of unreadable stack traces and a fully non-blocking chain, is now needed far less often.

How this shows up in real delivery

Java's real advantage on a long project is not performance — it is that the tooling assumes the code will outlive the people who wrote it. The build is reproducible, the dependency graph is explicit, refactoring in an IDE is mechanical and safe across a million lines, and a profiler ships with the JDK. Flight Recorder and Mission Control will tell you where the allocation and the pauses are on a production process, at a cost low enough to leave running.

Two operational notes worth having before your first production incident. Run an LTS release — 17 or 21 — because those get years of security updates while a feature release gets six months. And set the heap explicitly in a container: a JVM that thinks it has the host's memory will size its heap for the host and get killed by the orchestrator, which looks like a random restart and is not random at all.

Where it degrades

  • Lazy JPA relations loaded inside a loop, turning one endpoint into hundreds of queries.
  • A @Transactional method called from the same class, so the proxy is bypassed and nothing rolls back.
  • Exceptions caught and logged at every layer, so one failure produces five stack traces and no decision.
  • Abstraction added for a second implementation that never arrives — an interface and a factory per class.
  • A reactive stack adopted for scalability while one blocking driver in the chain serialises everything.
  • Heap size left to the default inside a container, which ends as an out-of-memory kill from the orchestrator.
  • Benchmarks taken before the JIT has warmed up, which measure the interpreter and prove nothing.

When to use it

Use it when

  • Long-lived systems with complex domains, where static types and safe refactoring pay back every year.
  • Services that must use every core in one process — real threads make that possible without extra processes.
  • High-concurrency APIs on Java 21 — virtual threads give reactive-level scale with ordinary blocking code.
  • Regulated environments, where an LTS release with years of security patches is a requirement rather than a preference.

Avoid it when

  • Short-lived functions that scale to zero, unless you are prepared to build and maintain a native image.
  • Small scripts and one-off tooling, where the ceremony costs more than the whole task is worth.
  • Reactive frameworks chosen for scale on Java 21, where virtual threads reach the same place far more simply.
  • Any environment where a garbage-collection pause of milliseconds is genuinely unacceptable — that is not this runtime.

Found this useful?

Share it with someone who is working on the same problem.