Skip to content
On this page0%0%
  1. Java, minus almost everything
  2. The memory model is upside down
  3. Power loss is not an edge case
  4. Debugging through a two-byte straw
  5. The crypto menu is fixed
  6. A ghost town that runs the world
  7. Why the fossil survives
  8. Coda
  9. Sources
All dispatches
18 min read

Programming Like It's 1999: JavaCard Development in 2026

The platform guarding your SIM card, bank card, and passport runs a dialect of Java frozen around the turn of the millennium—and many of its constraints look like 2026 best practice.

javacard
java
embedded
security
smartcards

The platform guarding your SIM card, your bank card, and your passport is a dialect of Java that froze around the turn of the millennium. I write code for it. It is the strangest developer experience in mainstream computing — and more of it is 2026 best practice than anyone wants to admit.


Here is a line of code I wrote this month, in the year 2026:

short off = (short)(cdataOff + sigLen);

Both operands are short. The result is a short. The cast is mandatory anyway, because Java’s arithmetic promotes everything to int — and on this platform, int is an optional feature. Not deprecated. Not discouraged. Optional, the way heated seats are optional. The specification’s exact words: the 32-bit int type “need not be supported in a Java Card implementation.” The compiler cannot assume your target hardware has it, so you confess your intentions with a cast on every arithmetic expression, forever.

Welcome to JavaCard: a subset of late-90s Java, running on a secure microcontroller with a few kilobytes of RAM, inside the chip of nearly every SIM and eSIM (the ETSI standard API for SIM applications is, literally, a Java Card API), most chip payment cards, and many of the world’s passports. Secure elements ship at a rate north of nine billion units a year, and Oracle has put Java Card’s share at close to six billion devices annually. It is quite possibly the most widely deployed application platform that almost nobody has written code for.

The programming model you target today was fixed by the 2.1 release in 1999 — the version that defined the card-application file format and the constraints still in force — roughly while The Matrix was in theaters. This is the story of what it’s like to develop for it in 2026, and why I’ve slowly come around to the view that the fossil is right and the rest of us are wrong.

Java, minus almost everything

The first thing JavaCard does is take Java away from you.

No String. No float, no double — and no long either; 64 bits is science fiction. No char, which is fine, because there is no text. No threads. No reflection. No lambdas, generics, or autoboxing, obviously, but also: no multidimensional arrays. No enhanced for loop, for the most on-brand reason imaginable — the spec notes that for-each requires int array indexing, and arrays here are indexed with shorts. The java.lang you get is Object, Throwable, and a short list of exceptions. There is no standard library in any meaningful sense — there’s Util.arrayCopy and its siblings, and there’s the crypto API, and that’s roughly the estate.

What’s left is not “Java on a card.” It’s C’s discipline wearing Java’s syntax, plus a bytecode verifier. You think in buffers and offsets. Every APDU handler — the card’s unit of interaction with the world — is a function that receives a byte array and hand-parses it with short indices:

public void process(APDU apdu) {
    byte[] buf = apdu.getBuffer();
    switch (buf[ISO7816.OFFSET_INS]) {
        case INS_SIGN:
            if (!pinValidated())
                ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
            // ...
    }
}

Modern cards mostly do support int (the ones I use do), but the platform’s center of gravity is 16-bit, and the maximum value of a short — 32,767 — is a number you will come to know intimately, usually via an offset calculation that wrapped negative somewhere in a buffer routine.

One more thing, as a mood-setter: the ISO status word for success is 0x9000, which as a JavaCard short is a negative number. Success is negative. You stop noticing after a few months.

The memory model is upside down

On every platform you have ever used, memory is ephemeral by default and persistence takes work. You call new, you get RAM, and if you want the data to survive a restart you serialize it somewhere deliberately.

JavaCard inverts this completely. The object heap lives in non-volatile memory. When you write new byte[32], that array is allocated in EEPROM or flash. It survives power loss. It survives years of power loss. Objects are forever by default, and it’s volatile memory that requires a special request:

private ExampleApplet() {
    // RAM: must be requested explicitly, wiped when the card leaves the field
    scratch = JCSystem.makeTransientByteArray((short) 64, JCSystem.CLEAR_ON_DESELECT);

    // Flash: 'new' means "carve this into the chip, permanently"
    state = new byte[32];
    key   = (ECPrivateKey) KeyBuilder.buildKey(
                KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false);
}

That constructor runs exactly once, at installation, and it is the closest thing you get to malloc — your allocation budget for the life of the applet, which may be a decade. The universal idiom is to allocate every object, buffer, and key container up front and never allocate again. Not as an optimization. As the only sane way to live, because there is no free(). The garbage collector is not slow, or unpredictable — it is not promised to exist. The virtual machine spec is admirably direct: “Any object allocated by a virtual machine may continue to exist and consume resources even after it becomes unreachable.” What exists instead is JCSystem.requestObjectDeletion(), an API whose name radiates its true nature: less a memory manager than a polite note left for the runtime, which the runtime is free to ignore.

The spec’s relationship with its own terminology is a genre unto itself. RAM-backed objects are called “transient objects,” about which the runtime specification remarks, deadpan, that the term “is a misnomer” — the object is as permanent as any other; only its contents evaporate. Meanwhile, Java’s actual transient keyword sits on the unsupported list: the language feature named for the platform’s defining concept is the one piece of Java it doesn’t accept. And the hardware being described has drifted a hundredfold from the description: a mainstream developer card today offers roughly 4 KB of transient memory and 180 KB of flash (top-end government-ID silicon reaches 450 KB), while the current spec still opens by describing a “typical resource-constrained device” as having 1.2 KB of RAM and 16 KB of non-volatile memory — a sentence essentially unchanged since the 1990s. The chips grew. The worldview didn’t. The worldview is the product.

Persistent memory also wears out. The endurance figures live in NDA’d datasheets, but the numbers engineers trade are in the hundreds of thousands of write cycles per cell — enormous until you realize that an innocently placed counter in persistent memory, incremented on every operation, is a wear failure in slow motion. Hot data goes in transient buffers. Flash writes are something you ration.

Power loss is not an edge case

A smartcard transaction can end at any moment. Not fail — end. The card is powered by the reader’s field, the user taps and walks away, and the world simply stops between two instructions. No shutdown hook. No signal handler. Silence. The runtime spec names the threat plainly: atomicity must hold against “power loss in the middle of a transaction.”

The platform’s answer is the most quietly impressive thing in it: transactions as a first-class runtime feature, in mass production since the early 2000s.

JCSystem.beginTransaction();
balance = newBalance;                                   // persistent write
Util.arrayCopy(newKey, (short) 0, keyStore, (short) 0, (short) 32);
JCSystem.commitTransaction();   // all of it happens, or none of it did

Rip the card off the reader mid-arrayCopy and, on next power-up, the runtime rolls back to the last consistent state. Individual persistent writes are atomic even outside transactions; Util.arrayCopyNonAtomic exists as the explicitly labeled escape hatch for scratch data that doesn’t need the bookkeeping. The commit buffer is finite and small — small enough that the API lets you query its remaining capacity byte by byte — and there is exactly one transaction, ever; nesting one inside another throws.

Consider what this means: the mainstream software world spent the last two decades painfully internalizing crash-only design, write-ahead logs, and idempotent operations. The chip in your debit card shipped with torn-write protection as a language primitive while we were still teaching each other not to parse HTML with regex.

Debugging through a two-byte straw

Here is the development loop. You compile with ordinary javac — though the converter accepts nothing newer than JDK 10 class files, which around here counts as practically a preview feature. The converter distills your classes into a CAP file — the interoperable card-application format introduced in 1999 and still the unit of deployment. An off-card verifier proves the bytecode well-formed. Then GlobalPlatform tooling authenticates to the card over a USB reader, loads the CAP, and you tap a card to talk to your code.

Modernity arrives, but cosmetically. Oracle’s dev kit now wears year-based version numbers like everything else, gained official VS Code support in 2025, and requires JDK 25 to run its tools — tools that then build for the 2015 edition of the spec, because that’s what retail cards implement. The simulator’s officially supported operating system is Windows 11.

And when something goes wrong, the card tells you everything it is ever going to tell you: two bytes.

ISOException.throwIt((short) 0x6985);   // this is your logging framework

There is no stack trace. There is no printf. There is no debugger attached to real silicon — deliberately: a chip you can single-step is a chip an attacker can single-step. Your entire observability stack is the ISO 7816 status word, and debugging is the art of inferring a state machine through a two-byte straw. 0x6985, “conditions not satisfied,” almost always means you’re in the wrong phase of a protocol — an operation invoked before its prerequisite, a state flag you forgot to set. 0x6D00, “instruction not supported,” feels like your dispatch table is broken; in my experience it more often means the wrong version of your applet is installed — the card is fine, and your provisioning pipeline is serving yesterday’s CAP file. The error channel cannot distinguish “your code is wrong” from “the wrong code is present,” and learning to tell them apart from context is simply what expertise means here.

The ecosystem around the loop has its own hazards, all of them permanent. Applet packages import each other by AID — hex identifiers you assign — and the card enforces the dependency graph with bureaucratic rigor: delete a shared library before the applets that import it and you can wedge the card; rename a package AID between versions and every deployed card becomes structurally unable to load the update, because its on-card imports point at a name that no longer exists. There is no --force flag. Some mistakes are just final, which is a category of mistake most modern developers have never met. The classic initiation ritual involves the card manager keys: fresh cards ship with the public default keyset (40 41 42 … 4F), and the standard tooling’s wiki warns — twice — that too many tries with incorrect keys “can irreversibly lock (brick) your card!” Usually three to ten attempts. A card that has concluded you are an attacker does not entertain appeals. Every JavaCard developer owns a small graveyard of bricked plastic, and the graveyard is the curriculum.

Simulators exist — jCardSim runs applets on desktop Java and is a genuine gift for unit tests — but a simulator is a JVM-hosted impersonation, agreeable in ways silicon is not: generous memory, permissive timing, complete algorithm support. The card is the only honest test environment, which means the last mile of every project is conducted two bytes at a time.

The crypto menu is fixed

Why put up with any of this? Because of what sits behind the API. One call —

sig = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false);

— and you’re driving a hardware crypto accelerator: constant-time, side-channel-hardened, fault-resistant, evaluated by government labs against attackers with lasers and electron microscopes. Key material can live its entire life inside the chip, generated on-card, never once representable in your application’s address space. For the cost of some short casts, you get a physics-backed security boundary smaller than your fingernail. That’s the trade, and it’s a good one.

But the menu is fixed. The API defines the algorithm list; each card implements a subset (every algorithm is individually optional — portability means probing for NO_SUCH_ALGORITHM and having a plan B); and if the primitive you need isn’t on the menu at all, you are now the chef. Out of curiosity, I once implemented BLAKE2b — a hash function from 2012, standard fare on any other platform — in pure JavaCard bytecode. After unrolling the hot loop into straight-line 32-bit operations, one compression block cost about 144 milliseconds. A full digest over a transaction-sized message: about 1.15 seconds. That’s achievable — for a signing ceremony where the card verifies what it’s signing, you might even call it acceptable — but you are computing at a pace the platform plainly considers impolite. “Milliseconds per compression block” is a unit of measurement that recalibrates your soul.

The menu does grow — on a geological clock, with the lag built in as a feature. The spec itself is livelier than its reputation: the 2019 release added Ed25519, X25519, HKDF, and applications larger than 64 KB; the 2023 release added the TLS 1.3 key schedule. But you don’t program against the spec; you program against the card, and the cards at retail run the 2015 edition. As for the post-quantum era, it is arriving exactly the way everything arrives here: vendors first, API later. There is still no post-quantum algorithm in the Java Card API — yet in January 2025 Infineon obtained the world’s first Common Criteria certification of a lattice-based KEM on a security controller, and in March 2026 Thales demonstrated upgrading already-deployed 5G SIM cards to quantum-safe crypto over the air. Cards in the field acquiring new mathematics by radio, years before the official API admits that mathematics exists: that is this platform’s metabolism in a single image.

A ghost town that runs the world

The community, such as it is, would fit in a seminar room — and periodically does. The Java Card Forum, the industry body that steers the specification, consists of seven member companies plus Oracle; it convenes for two days every six months with, by its own description, about twenty people attending. Twenty people, twice a year, governing the programming platform of a distressingly large fraction of all deployed chips on Earth.

Independent development runs on an open-source toolchain maintained, to a first approximation, by one extremely dedicated developer in Estonia — the standard build tool and the standard card-loading tool are both his — plus an open-source simulator from a small security company. The applet ecosystem is niche but conspicuously alive: the French national cybersecurity agency’s OpenPGP applet cut a release two days before I wrote this paragraph; an open-source FIDO2 authenticator, a couple of hardware-wallet applets, and the PIV and eID implementations have all seen commits this summer. The Stack Overflow tag, meanwhile, is a museum. It’s a strange demography — no crowd, no churn, no hype cycle, just a few dozen people quietly maintaining load-bearing civilization.

And it is load-bearing. Every phone that attaches to a cellular network authenticates through this platform. Every chip-card payment. Border control. The gap between how much of daily life depends on JavaCard and how many living humans can write it is, as far as I can tell, the widest such gap in computing.

Why the fossil survives

It’s tempting to file JavaCard next to COBOL: legacy platform, too entrenched to die. I think that’s wrong. COBOL survives because replacing it is expensive. JavaCard survives because three of its properties are load-bearing, and each one is a consequence of the stagnation everyone mocks.

The certification moat. A secure element doesn’t get deployed because a product manager likes it. Chip and platform go through Common Criteria evaluations — the current NXP and Infineon flagship platforms are certified at EAL6/EAL6+, including the highest vulnerability-analysis bar there is (AVA_VAN.5: methodical penetration testing by evaluators, with required resistance against attackers formally modeled as having “high attack potential” — funded labs with fault injectors and probing stations). Payment deployments stack EMVCo approval on top; Europe is currently migrating the whole edifice to its new EUCC certification scheme. These evaluations take years, and each one is anchored to a specific, frozen platform. Novelty resets the clock. An exciting new runtime with annual breaking releases is, in this world, not just unattractive — it is uncertifiable. The platform’s refusal to churn isn’t a failure to innovate; it is the precondition for the entire trust chain above it.

The security model was right — decades early. Strip the retro syntax and look at the architecture: memory-safe bytecode, verified before execution; applets from mutually distrusting parties isolated by a runtime-enforced firewall, sharing data only through explicit, narrow interfaces; no dynamic code loading outside the audited install path. The runtime spec is disarmingly honest about the firewall’s purpose: it guards against “developer mistakes and design oversights” — the platform assumed, in the early 2000s, that its own developers would ship bugs, and engineered accordingly. JavaCard was shipping verified, memory-safe, capability-isolated code into billion-unit production while Windows 98 users were double-clicking email attachments. The record isn’t pristine — researchers periodically surface VM-level bugs, and 2019 brought a notable batch — but the industry spent twenty-five years reinventing this exact agenda under the banner of modernity: sandboxing, memory safety, least privilege, verification.

The constraints are honest. No allocation after startup. Atomicity as a first-class API. Persistence you must budget. Failure modes forced into your face at design time rather than discovered in production. Every one of these reads as an archaism until you set it next to a safety-critical coding standard or an embedded-Rust style guide, at which point JavaCard starts to look less like a fossil and more like a platform that arrived at defensive programming before the rest of us had anything worth defending.

There’s a lesson here that generalizes, and it’s uncomfortable for an industry that measures health in release velocity: in security, churn is attack surface. A platform that changes slowly can be fully known. A platform that can be known can be certified. A platform that can be certified can be trusted with billions of chips a year. Stability isn’t the absence of progress. In this corner of computing, stability is the deliverable.

Coda

So yes: it is 2026, and I spend my days casting shorts, rationing flash writes, budgeting allocations at install time, and receiving my errors two bytes at a time, more or less exactly as a Schlumberger engineer did when the platform was announced in October 1996. Programming like it’s 1999.

But here’s the thing about the card in your wallet. It has no battery and never needs charging. It has never asked you to update it, never shown a spinner, never presented new terms and conditions. It has outlived several of your phones. Decades of platform fads have washed over the industry — and will keep washing — while it sits in the dark, holding keys, waiting.

Tap. 9000. Like it’s 1999.


Sources

Specifications and platform (Oracle):

Scale:

Shipping hardware:

Toolchain:

Ecosystem:

Certification & PQC:

History: