Evaluate a Java-to-Clojure migration with behavior, maintainability, performance, operability, and team-learning evidence instead of relying on broad claims about code reduction or functional programming.
A migration outcome is only useful if it can be reviewed. Claims such as “Clojure made the application faster” or “the codebase is simpler” need evidence: behavior comparisons, code review notes, production metrics, operational incidents, and team feedback.
This final case-study page shows how to evaluate the account summary slice after it has run behind a Java adapter and passed shadow comparison.
Measure more than lines of code.
| Category | Evidence standard |
|---|---|
| Behavior | Prefer fixture parity, mismatch logs, and approved intentional differences. Do not rely on “the tests passed once.” |
| Maintainability | Prefer smaller decision surfaces, clearer data contracts, and easier review. Do not rely on raw line-count reduction. |
| Performance | Prefer latency percentiles and allocation profiles for the migrated path. Do not rely on anecdotal local timing. |
| Operability | Prefer feature flag behavior, error visibility, and rollback practice. Do not rely on “no one complained.” |
| Team learning | Prefer review quality, pairing notes, and documented patterns. Do not leave the path maintainable by only one expert. |
For Java engineers, this framing keeps the migration disciplined. Clojure is not a success because it is different; it is a success when the new boundary is easier to reason about and operate.
The most important result is behavioral confidence.
| Check | Result to record |
|---|---|
| Golden fixtures | Old Java and new Clojure returned the same summaries for representative accounts. |
| Edge cases | Empty order lists, null-adjacent Java inputs, zero balances, and boundary money values were covered. |
| Intentional differences | Any changed warning wording or rounding behavior was reviewed explicitly. |
| Shadow mismatches | Mismatches were logged with input identifiers, not silently ignored. |
If the Clojure function is pure, mismatches are usually easier to reproduce than they were in the original service method.
1(deftest summarizes-over-limit-account
2 (is (= {:account/id "A-100"
3 :account/open-balance 1150M
4 :account/over-limit? true
5 :account/warnings ["Past due order: O-1"]}
6 (summarize {:account/id "A-100"
7 :account/credit-limit 1000M}
8 [{:order/id "O-1"
9 :order/outstanding-amount 250M
10 :order/past-due? true}
11 {:order/id "O-2"
12 :order/outstanding-amount 900M
13 :order/past-due? false}]))))
The test reads like a business example. That is a real maintainability gain.
Do not reduce maintainability to fewer files. The useful question is whether a reviewer can see the business rule without traversing framework setup, mutation, and side effects.
| Before | After |
|---|---|
| Service method mixed repository calls, accumulation, warning construction, and notification decisions | Java service kept repository calls; Clojure function owned the summary decision |
| Tests required service wiring or mocks | Core tests used maps and expected values |
| Side effects were implicit in the service body | Planned notification commands were explicit values |
| Review focused on control flow | Review focused on input contract, output contract, and edge cases |
The migration did not eliminate Java. It gave Java a clearer orchestration role and gave Clojure a clearer decision role.
Performance should be measured with production-shaped inputs. Clojure’s persistent data structures, laziness, and sequence abstractions are powerful, but they still need measurement when the path is hot.
| Metric | What to compare |
|---|---|
| Request latency | Java-only path versus Java adapter plus Clojure core at p50, p95, and p99. |
| Allocation profile | Extra map conversion cost and lazy sequence realization. |
| Throughput | Batch or request volume under representative input sizes. |
| Warmup | JVM behavior after deploy, not only after a local REPL session. |
| Failure rate | Adapter conversion errors, nil handling, and numeric conversion issues. |
If a migrated pure function is not on a hot path, maintainability and correctness may matter more than micro-optimization. If it is on a hot path, profile before applying transducers, type hints, or primitive arrays.
| Lesson | Why it matters |
|---|---|
| Stable Java boundaries reduce organizational risk | Existing callers and operations teams do not need to absorb every change at once. |
| Plain maps need contracts | Keyword names, required fields, and numeric types must be documented and tested. |
| Shadow mode must suppress duplicate effects | Comparisons are useful only if they do not send duplicate emails or writes. |
| Small Clojure namespaces teach better than large rewrites | Java engineers can review one pure function and one adapter at a time. |
| Cleanup is part of the migration | Old Java paths should be removed only after evidence and ownership are stable. |
The strongest lesson is that migration discipline matters more than language enthusiasm.
After the first slice succeeds, choose the next slice by evidence, not by momentum.