Validate migrated Clojure performance after Java replacement with explicit budgets, representative workloads, warmed JVM runs, allocation checks, profiling evidence, and regression gates.
Post-migration performance testing answers one narrow question: does the migrated Clojure path meet the budget that matters for this system? It should not be a generic contest between Java and Clojure. It should be a controlled comparison of the old and new behavior under representative workload.
The previous page proved equivalence. This page checks whether the equivalent behavior is fast enough, stable enough, and observable enough to ship.
Do not start by optimizing. Start by defining what acceptable performance means.
| Budget | Example question |
|---|---|
| Latency | Must p95 stay below 120 ms for the migrated endpoint? |
| Throughput | Must the batch job process 50,000 records per minute? |
| Allocation | Did the new path increase garbage collection pressure beyond the service budget? |
| Startup | Does cold-start time matter for this deployment model? |
| Error rate | Did conversion failures or timeouts increase after migration? |
| Resource use | Did CPU, heap, or thread count exceed capacity assumptions? |
A migrated path can be slightly slower and still be acceptable. It can also be faster locally and unacceptable in production. Budgets make that distinction explicit.
Performance tests fail when the workload is too clean.
| Workload detail | Why it matters |
|---|---|
| Input size distribution | A median-sized input may hide p99 behavior. |
| Boundary values | Money, dates, empty collections, and large collections can stress different code paths. |
| External dependencies | Database, cache, HTTP, and queue latency may dominate the result. |
| Warmed JVM state | JIT compilation and class loading affect early measurements. |
| Feature flags | Old and new paths must be measured under the same routing conditions. |
Keep a small set of local benchmarks for developer feedback, but use production-shaped load tests before cutover.
For a pure Clojure function, a local benchmark can isolate transformation cost.
1(ns migration.summary-bench
2 (:require [criterium.core :as criterium]
3 [migration.summary :as summary]))
4
5(def orders
6 (vec (for [idx (range 10000)]
7 {:order/id (str "O-" idx)
8 :order/amount 42M
9 :order/rejected? (zero? (mod idx 17))})))
10
11(criterium/quick-bench
12 (summary/summarize orders))
This does not replace system testing. It tells you whether the pure transformation is obviously expensive before you blame databases, queues, or HTTP.
Use profiling evidence to choose the optimization target.
| Symptom | First investigation |
|---|---|
| CPU is high | Capture a CPU profile and identify hot functions or Java interop calls. |
| Garbage collection increased | Check allocation rate, intermediate sequences, large maps, and conversion copies. |
| Tail latency regressed | Inspect locks, queues, external calls, and outlier input size. |
| Startup regressed | Separate class loading, namespace loading, and application initialization. |
| Throughput regressed under concurrency | Check state coordination, executor use, blocking calls, and connection pools. |
Do not add type hints, transients, or primitive arrays until the profile tells you they address the real cost.
Performance tests should influence release decisions.
| Gate | What it prevents |
|---|---|
| Equivalence gate | Shipping faster code that changed behavior. |
| Local benchmark trend | Missing obvious pure-function regressions during development. |
| Load test budget | Shipping a path that fails production-shaped demand. |
| Allocation check | Introducing hidden garbage collection pressure. |
| Observability check | Shipping without enough metrics to debug production behavior. |
CI does not need to run every expensive benchmark on every commit. Use a bounded gate for ordinary changes and a deeper gate before cutover or large migration slices.
| Result | Decision |
|---|---|
| Meets budget and is easier to maintain | Ship with normal monitoring. |
| Slightly misses budget but bottleneck is clear | Optimize the measured bottleneck and rerun. |
| Misses budget because of external dependency wait | Fix orchestration or dependency behavior before rewriting Clojure. |
| Faster but behavior differs | Stop and fix equivalence first. |
| Faster locally but slower under load | Re-check concurrency, warmup, I/O, and production input distribution. |
Performance validation is part of risk management. It is not a reason to micro-optimize every migrated function.