skip to content

When would you choose CRaC over GraalVM native image (or vice versa) for a Spring Boot service?

level: principalimportance: should knowfreq 30%

answer

  1. Native = AOT closed-world, low memory, no JIT
  2. CRaC = snapshot warmed HotSpot, full throughput, Linux-only
  3. CRaC keeps dynamic features + tooling; native constrains them
  4. CRaC image holds heap = secrets on disk
  5. Native wins memory density / scale-to-zero; CRaC wins throughput

basics

~20 s

Choose CRaC when you need fast startup but must keep full JIT throughput, dynamic behavior (reflection/agents), and easy builds — and deploy on Linux. Choose native image when you need lowest memory and startup and can accept closed-world build constraints and lower peak throughput.

solid answer

~40 s

Both attack slow JVM startup, but differently. **Native image** AOT-compiles a closed-world binary on Substrate VM: tiny memory footprint, ~tens-of-ms startup, but long builds, reflection/proxy/resource config burden, no JIT so lower peak throughput, and limited runtime tooling. **CRaC** snapshots a real, warmed HotSpot JVM: near-instant restore AND full JIT throughput, standard tooling, no closed-world constraints — but images are Linux/CRIU-bound and architecture-specific, contain your heap (secrets on disk), require resource close/reopen coordination, and need a warm-up step in the build for peak benefit. Pick CRaC for throughput-sensitive services, heavy dynamic behavior, or when native-image reachability config is impractical. Pick native image for memory-constrained, high-density, or scale-to-zero serverless workloads where footprint dominates and peak throughput is secondary. Both need Linux delivery; CRaC uniquely preserves warmed performance.

go deeper

for a junior

Know both speed up startup; native = compiled binary, CRaC = JVM snapshot.

for a middle

List the core trade: native low memory / lower throughput vs CRaC full throughput / Linux-only.

for a senior

Map workloads to choice (serverless/memory vs high-QPS/dynamic) and note Spring AOT is required only for native.

for a principal

Own the full operational and security calculus — build warm-up, image-as-secret, resource coordination, platform lock-in — and set an org-wide default with exceptions.

## Same goal, opposite mechanisms Both technologies exist because a plain JVM is slow to start and slow to warm up. But they get there by opposite means: | Dimension | GraalVM Native Image | Project CRaC | |---|---|---| | Mechanism | Ahead-of-time compile a closed-world binary (Substrate VM) | Snapshot a running HotSpot JVM (CRIU) and restore | | Startup | ~tens of ms | ~tens of ms (restore) | | Peak throughput | Lower — no JIT, PGO helps but not equal | Full — warmed JIT code is in the image | | Memory footprint | Very low (biggest win) | JVM-level (heap is restored) | | Build | Slow, memory-heavy AOT build | Normal build + a warm-up/checkpoint step | | Dynamic features | Constrained: reflection/proxies/resources need reachability config (`RuntimeHints`, Spring AOT) | Unconstrained: full reflection, agents, dynamic class loading | | Tooling at runtime | Limited (no JVMTI agents, restricted JFR) | Full JVM tooling (JFR, agents) | | Portability | OS/arch-specific binary | Linux-only, arch-specific, CRIU-bound image | | Security surface | Code compiled in; less runtime state on disk | Heap (incl. secrets) serialized to disk image | ## Decision drivers **Choose native image when:** - **Memory density** is the priority — many small instances, high pod density, tight per-instance RAM. This is native image's clearest, unmatched advantage. - **Scale-to-zero serverless** where cold-start footprint and start time dominate and each request volume is modest (peak JIT throughput matters little). - You can absorb the **closed-world constraints** — Spring's AOT engine and `RuntimeHints` generate most config, but exotic reflection/dynamic proxies/classpath tricks still need manual hints, and builds are slow. **Choose CRaC when:** - **Peak throughput matters** — latency-sensitive or high-QPS services where a native image's missing JIT would hurt. CRaC restores *already-warmed* code. - The app uses **heavy dynamic behavior** (extensive reflection, bytecode agents, dynamic class loading, JVMTI tooling) that makes native-image reachability config impractical. - You want **fast startup with minimal build/compat risk** — you keep the ordinary JVM, ordinary libraries, ordinary debugging. - You deploy on **Linux** (mandatory) and can add a warm-up step to the pipeline. ## The hidden operational costs of CRaC - **Warm-up in the build**: the full benefit (JIT-warm restore) requires running representative traffic before `jcmd JDK.checkpoint`. That means a load harness in CI, which is non-trivial to make representative. - **Image = sensitive artifact**: the serialized heap holds whatever was in memory — secrets, tokens, connection credentials. Every restored instance is a clone. Refresh short-lived secrets in `afterRestore`; secure the image at rest. - **Resource coordination**: sockets/pools/files must be closed before and reopened after (the `org.crac.Resource` contract). Third-party libs without CRaC awareness may block a clean checkpoint. - **Determinism hazards**: restored clones share RNG state, cached timestamps, and any identity captured at checkpoint — re-seed/re-derive on restore. - **Platform lock-in**: Linux + CRIU + a CRaC-enabled JDK (Azul/BellSoft). No macOS/Windows checkpointing. ## They are not mutually exclusive with Spring AOT Spring's **AOT engine** (the `spring-aot` processing that native image relies on) is orthogonal — it optimizes context startup and is *required* for native image, *optional* for CRaC. You can also run a plain JIT JVM with neither. The three-way choice is: plain JVM (simplest, slow start), CRaC (fast start + full throughput, Linux, heavier ops), native image (fastest+smallest, lower throughput, build constraints). ## A pragmatic default For most throughput-oriented backend microservices on Linux/Kubernetes that want faster rollouts and autoscaling without a native-image migration, **CRaC is the lower-risk path to fast startup while preserving performance**. Reserve native image for memory-bound, high-density, or true scale-to-zero serverless tiers.

  • Is Spring AOT processing required for CRaC?
    No. Spring AOT is mandatory for native image but optional for CRaC — CRaC runs on an ordinary JVM. You can combine them (AOT to speed initial refresh) but CRaC does not depend on it.
  • For a high-QPS payment API needing fast rollouts on Kubernetes, which do you lean toward and why?
    CRaC — it gives fast startup for quick rollouts/autoscaling while preserving full JIT peak throughput and the standard JVM/tooling, avoiding native-image's throughput penalty and reflection-config risk. Kubernetes is Linux, satisfying CRaC's constraint.

saying these in an interview costs you the question

  • Claiming native image matches a warmed JIT on peak throughput
  • Saying CRaC's main advantage is memory footprint (that's native image)
  • Ignoring that CRaC serializes the heap/secrets to disk
  • Assuming either can checkpoint/build-run on macOS/Windows for deployment

context