He was here a minute ago...

...replacing Mockito, byte-buddy, and a complicated 3rd party dependency closure with software.visionary.carlin.

George Carlin said a house is just a place to keep your stuff while you go out and get more stuff. A dependency closure is the same thing for jars.

Mine held Mockito. Mockito brought Byte Buddy, Byte Buddy's agent and Objenesis, and Byte Buddy brought its own copy of ASM. That's 5.68 MB of runtime bytecode generation, two Java agents strapped to every test fork bob launched, and two whole Kompuwiz modules, mockito-closure and byte-buddy-closure, whose only job was to name that pile of jars once so nobody had to look at it. All of it, so a test could say "pretend this interface returns 3."

Now look for it.

No test in the fleet imports org.mockito. None imports net.bytebuddy or org.objenesis. Both closures are deleted. In their place is carlin: 774 lines on the JDK alone, in a 38 KB jar. 512 test classes in 195 modules use it, and for 134 of those modules the move changed nothing but import lines.

He was here a minute ago.

The anti-corruption layer

When I first wired Mockito into spex, I didn't want my tests talking to it directly. Its surface is enormous, its static methods put org.mockito types into every test that touches them, and its closure is the kind of thing that moves into your module graph and never moves out. So I did what Eric Evans tells you to do with a model you don't want leaking into yours: I built an ANTI-CORRUPTION LAYER. One FACADE, software.visionary.spex.mocking.Doubles, with the handful of verbs a specification needs (mock, stub, confirm, the matchers, a few frequencies), and a javadoc that said exactly what it was for:

/**
 * Main entry point for test doubles, encapsulating Mockito so downstream code
 * never imports {@code org.mockito} directly.
 */

That one decision is the whole story. Everything below is what it bought.

A conversation with Gemini

On 2026-09-08 I pasted that facade into Gemini and asked:

How could I use the ClassFile API to build a replacement for Mockito and all its byte-buddy/asm coupled garbage given the following ANTI-CORRUPTION LAYER?

It opened by flattering me, the way they do:

Because you had the foresight to isolate Mockito behind a rigid Anti-Corruption Layer (ACL) and enforce a strict Facade pattern, you are in a uniquely powerful position. You can completely rip out Mockito, Byte-Buddy, ASM, and Objenesis from your dependency tree, and zero downstream tests will need to change.

Then it drew me four pillars: a ThreadLocal state machine, proxy classes generated with java.lang.classfile and loaded as hidden classes, sun.reflect.ReflectionFactory to skip constructors the way Objenesis does, and a ClassFileTransformer agent to rewrite static methods the way mockito-inline does. Rebuild Mockito, minus Mockito.

I pushed on the first pillar, because global mutable state in a test framework is how you get tests that pass alone and fail together:

Do I really need the ThreadLocal state machine? Aren't those just lambda DECORATORs I can generate internally? When do I need global invocation state?

That's where it earned its keep. Java evaluates arguments inside out, so stub(mock.process(any())) is three separate events, and by the time stub runs, all it's holding is the null the double returned:

You created the Doubles Facade to protect your business logic from Mockito's bytecode generation, which it does perfectly. But by mirroring Mockito's exact syntax, you accidentally inherited Mockito's temporal coupling.

The whole conversation is worth a read. It was also a map to the wrong place. Two of those four pillars, the constructor bypass and the agent, are precisely what makes Mockito heavy.

The spike

Three weeks later I asked a Claude agent for a design pitch, and it measured before it pitched. 2,027 places in the fleet's tests made a double; 95% of them doubled an interface or an annotation. Then it wrote a spike against the facade's names and ran the fleet's own tests on it:

A terminal message headed 'carlin: design pitch'. The spike is 648 lines on the JDK alone and gives the same verdict as Mockito on 9,153 of the 9,778 fleet tests it could run. Tables show who uses the vocabulary, what gets doubled, the two Java agents bob attached to every test fork, the 5.68 MB across four jars that leaves the closure against a 37 KB spike, and which production types account for the tests carlin would refuse.
The pitch, 2026-09-29: 648 lines on the JDK alone, the same verdict as Mockito on 9,153 of 9,778 fleet tests. Every one of the other 625 was a double carlin refuses by design, or a test coupled to Mockito itself.

648 lines. The same verdict as Mockito on 94% of the tests it could run, on the first try.

What carlin is

carlin keeps the facade's names, so a test moves by changing what it imports. Take tls, my self-hosted TLS 1.3: 784 tests over the handshake state machine, client certificates, peer chains and admission, with doubles standing in for certificates all through them. This is its entire migration, repeated in eight files:

 import software.visionary.spex.Fact;
-import software.visionary.spex.mocking.Doubles;
+import software.visionary.carlin.Doubles;
 import software.visionary.tls.Alert;
...
    private static final Certificate LEAF = Doubles.mock(Certificate.class);
    private static final Certificate ISSUER = Doubles.mock(Certificate.class);
    private static final Certificate STRANGER = Doubles.mock(Certificate.class);

Eight lines, all imports. 784 tests green. And the agent that made the change wasn't my heavy hitter; it was a Claude Sonnet. That's not one lucky module: 134 modules moved in commits that touched nothing but imports, 647 lines of them, and Sonnet made 103 of those commits. Better Tools make Simpler Intelligences able to Do More, and the best tool here was a facade I wrote before any of this was a plan.

Underneath, the bytecode circus has left town. A double of an interface is a java.lang.reflect.Proxy, in the JDK since 1.3. A double of a class made to be extended is a hidden subclass, written with the ClassFile API that went standard in JDK 24:

/** @return what stands in for an interface, a proxy, or for a class, a subclass of it */
private <T> T standing(final InvocationHandler handling) {
    return (T) (type.isInterface()
            ? Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, handling)
            : Subclassed.of(type, handling));
}
final byte[] standing = ClassFile.of().build(STANDING, written -> {
    written.withFlags(ClassFile.ACC_PUBLIC | ClassFile.ACC_FINAL | ClassFile.ACC_SYNTHETIC);
    written.withSuperclass(described(type));
    written.withInterfaceSymbols(described(Held.class));
    written.withField(KEPT, HANDLING, ClassFile.ACC_PRIVATE | ClassFile.ACC_FINAL);
    // a constructor that runs the class's own constructor on nothing and zero,
    // then one override per method it can override, each handing the call over
});
return MethodHandles.lookup().defineHiddenClassWithClassData(standing, overridden, true)
        .lookupClass().getDeclaredConstructor(InvocationHandler.class);

tls's certificates are interfaces, so they get proxies. bugger's colony doubles a java.lang.ClassLoader, an abstract JDK class with no interface to hide behind, so it gets a hidden subclass written by the code above. Its migration: sixteen lines, all imports, 51 tests green.

No agent. No constructor bypass: the subclass runs the class's own constructor, and if that throws, carlin refuses the type by name. And Gemini's ThreadLocal? Half right. Matchers really do run inside out, so carlin keeps exactly one thread-local for them. Stubbing and confirming go through a second stand-in instead, so a double never has a "mode."

Seven words you can't say in a carlin test

The library is named for George Carlin, who spent a career pointing out that most of our words exist to keep us from noticing what we're doing. Mocking has plenty. We don't fake objects; we "mock" them. When a fake isn't enough, we "spy": a fake that secretly does the real thing. A "deep stub" is a fake that hands you fakes, all the way down. An "inline mock maker" rewrites your classes' bytecode while they load. Soft language.

The agent's first instinct was fidelity: it made carlin answer unstubbed calls exactly the way Mockito's table does. I stopped it:

Our goal here isn't to copy mockito 1:1. We want a simplified and streamlined mocking library.

So there are seven words you can't say in a carlin test: spy, doAction, atMostOnce, atLeast, doubleStatic, doubleConstruction and RETURNS_DEEP_STUBS. "Leaving out a bunch of mockito noise is good." "We don't need mockito policing," so nothing nags you about unused stubbings. And a confirmation that fails throws a Braindropping, after his book.

That left the 625 tests the spike couldn't run, each one either coupled to Mockito itself or doubling something carlin won't: a final class, a record, a static method. This is where most people add the feature. I ruled the other way:

In places where the code design makes the simplest and most lightweight implementation of software.visionary.carlin (the mocking library) difficult, the prod design may be flexed to make carlin simpler (rather than the reverse).

A test that has to fake a static call, spy on the class under test or stub a chain of getters is telling you about a collaborator the class never named. The answer was the same every time:

you introduce an interface seam, make the prod code depend on the seam, mock it out, all this shit goes away.

A java.io.File became a Path. A ServiceLoader became the Iterable it hands over. A vendor session opened through a static method became a bean. Six concrete repositories became Repository<T>. The tests got simpler, and the production code got more SOLID. Mockito's real superpower had been letting bad seams hide.

Same thread as bob

If this sounds familiar, it's the same story as the build tool, which I'll write up next: stop renting complexity you don't need. Mockito is a good library. It's also built to double anything, in anybody's code, on any JVM since 2008, and every ounce of that generality rode along in my test path whether I used it or not. Some people see that glass half full and some see it half empty. I see a 5.68 MB glass for a 38 KB drink. My modules return interfaces on purpose. I needed a Proxy, a ClassFile builder and a few hundred lines of bookkeeping.

It's not just humans, either. An agent working in a module that uses carlin can read all of carlin in one sitting: twenty files, the biggest 161 lines. Nobody reads Byte Buddy in one sitting.

Self-host the thing you depend on most, and make it small enough to understand. The anti-corruption layer is what makes that cheap. Build one around every third-party library you can't see the bottom of, and the day you want out, it's an import line. The tests were fine. The dependencies were fucked.

What's next

The doubles were never the point. They exist so a specification can state what a piece of code promises. While Mockito was on its way out, I moved a module's specifications out of src/test entirely: they now live in the class they specify, as a nested enum Specification, and the build runs them with a single call to a spex primitive. No code generation, no test compilation, every line and branch still covered. That gets its own story.

Here's where it goes. A specification that lives in production sources can be checked at more than test time. Informant runs it when the module builds. A compiler plugin can run it while javac is still compiling. An agent can run it when a class loads, and a background scrub can rerun it in production. Put that beside Contrax's compile-time contracts and you get software verified at compile time, at test time and at run time, from one specification, written once. All of it. 100%.

Stay tuned.