The Identifier That Knew Too Much (Id Series)

JMBG vs OIB

Published Voice Engineer Tags Id SeriesOpinions

Everyone from the former Yugoslavia knows a thirteen-digit number by heart. The number knows them back: birthday, sex, roughly where they come from — and it can prove it was typed correctly, offline, with pen and paper. Croatia replaced it with eleven digits that know nothing at all. Both were good engineering decisions.

This is part of the Id Series: how identifiers get designed — what goes into them, what stays hidden, and what every bit ends up costing. All entries live under #Id Series.

In Id Series #001 I argued that identifiers can deliberately carry useful information — timestamps, shard numbers, tenant ids, entity types. In Id Series #002 I warned against exposing meaningful identifiers outside the system, and mentioned JMBG as the cautionary example.

Those two positions look like they contradict each other. They don’t. And the cleanest way to show why is to compare two real, national-scale identifier designs that took the two philosophies to their logical extremes.

The boring thirteen-digit number

The JMBGJedinstveni matični broj građana, the Unique Master Citizen Number — was introduced by a federal law published at the end of 1976 and assigned across the Socialist Federal Republic of Yugoslavia from 1977 onward. Bosnia and Herzegovina, Montenegro, North Macedonia, Serbia, and Slovenia still use it today — under local names: EMŠO in Slovenia, EMBG in North Macedonia, JMB in Montenegro. Same layout, same checksum: one design, living under four names, in five states, three decades after the country that issued it stopped existing.

Most people who have one have written it thousands of times and never once thought of it as a designed data structure. It looks like a serial number. It is not. It is a record format.

There is a database row hiding in this number

The thirteen digits are laid out like this:

DD MM YYY RR BBB K
│  │   │   │   │ └─ check digit
│  │   │   │   └── serial number (000–499 male, 500–999 female)
│  │   │   └────── political region of registration
│  │   └────────── year of birth (last three digits)
│  └────────────── month of birth
└───────────────── day of birth

Take a synthetic example — one that no correctly issued number can match:

2902990 71 123 3

Without a database, without an API, without a network connection, you can read from the string itself: born on 29 February 1990, registered in region 71 (Belgrade), serial 123 — which is below 500, so male — and the final 3 confirms the number is internally consistent. Now check a calendar: 1990 had no 29th of February. The law defines those first seven digits as the holder’s date of birth, and this date of birth never existed — so no person can carry this number correctly; the worst case is a clerical error somewhere, the same class of mistake that occasionally produced officially issued numbers with wrong check digits. That is the strongest claim thirteen digits allow, and it is why this example is safe to print: pick any plausible date and region instead, and you have most likely written down a real person’s number. The space is that dense.

The region blocks partitioned the whole federation: 10–19 for Bosnia and Herzegovina, 20–29 for Montenegro, 30–39 for Croatia, 41–49 for Macedonia, 50 for Slovenia, 70–79 for central Serbia, 80–89 for Vojvodina, 90–99 for Kosovo, with low codes reserved for foreigners.

That is a date of birth, a sex, a coarse location, a uniqueness guarantee within that partition, and error detection — in thirteen decimal digits. The number is a tiny serialized record. Issuing offices across the entire country could generate them without coordinating with each other on anything except their own regional serial counters, and any clerk anywhere could decode and sanity-check one with no infrastructure at all.

In 1977, “no infrastructure at all” was not a nice-to-have. It was the deployment environment.

Thirteen digits is ridiculously small

A short detour into information density, because the numbers here are worth being precise about.

Thirteen decimal digits can express 10¹³ distinct values. That is less than 2⁴⁴ — under 44 bits of numerical state. Eleven digits is under 37 bits. Both fit into a 64-bit integer with room to spare. Compare:

JMBG        13 decimal characters    < 44 bits of state
OIB         11 decimal characters    < 37 bits of state
Int64        8 bytes                   64 bits
UUID        16 bytes                  128 bits
UUID text   36 characters

Note that these are four different measurements that people habitually conflate: the theoretical information content (bits of state), the in-memory representation, the database column, and the textual form that travels over networks and gets read over the phone. A UUID is 16 bytes in memory and 36 characters as canonical text. A JMBG is 13 characters everywhere, and a human can hold it in short-term memory long enough to type it.

Is thirteen digits actually minimal, though? Not even close. Yugoslavia had around 22 million citizens; a bare serial number with tenfold headroom fits in eight digits, nine with a check digit. By that measure both designs are wasteful: JMBG spends seven of its thirteen digits on the birth date alone, and OIB parks a few million persons and legal entities in a space of ten billion numbers, leaving it more than 99.9% empty. (A side effect worth knowing: a random string that passes the OIB checksum is still almost certainly not an issued OIB.)

So the format is “wasteful” twice over — and the obvious fix, mixing letters into the alphabet, was sitting right there. Why did nobody take it?

An alphabet designed for humans

The obvious objection to letters — look-alikes like 0/O and 1/I/l — turns out to be the weakest one, because it has a known, standardized fix: prune the alphabet.

The cleanest example is Crockford’s base32 — yes, that Crockford: Douglas Crockford, the JSON guy. Take the ten digits and the twenty-six letters, then throw out the four troublemakers:

0 1 2 3 4 5 6 7 8 9
A B C D E F G H _ J K _ M N _ P Q R S T _ V W X Y Z

I, L  excluded — decoded as 1 if seen
O     excluded — decoded as 0 if seen
U     excluded — so a generated id cannot accidentally spell an obscenity

Thirty-two case-insensitive symbols, a clean five bits per character. The decoder is deliberately forgiving: hand it a lowercase l or a capital O and it silently reads 1 and 0 — the design assumes a human typed it, and covers for them. There is even an optional check symbol, computed mod 37.

And this is not one eccentric’s hobby. Vehicle VINs (ISO 3779) ban I, O, Q. Bitcoin’s Bech32 addresses drop 1, b, i, o and add error correction on top. Google’s Plus Codes use twenty symbols picked to dodge look-alikes and to be unable to spell a word in any major language. Wherever a standard expects humans to read, write, or shout an identifier across a room, the alphabet shrinks.

In Crockford’s base 32, JMBG’s whole state space fits in nine characters and OIB’s in eight. Concretely, re-encoding our two synthetic examples:

JMBG   2902990711233   →   2MFKVNTE1    13 chars → 9
OIB    12345678903     →   0BFXR71Q     11 chars → 8

Same values, four and three characters shorter — and if you were designing from scratch instead of re-encoding, a bare serial number for a billion people fits in six characters, seven with a check symbol. That would have covered the entire federation forty times over.

So why did neither design take the deal? Two constraints that no amount of alphabet pruning fixes.

First, Yugoslavia ran on two official scripts, and no amount of alphabet pruning fixes that: any subset of Latin letters is still Latin. Digits are the only symbols shared by Latin and Cyrillic typewriters, forms, and handwriting alike. (The two scripts do share a handful of same-glyph, same-sound letters — A, E, J, K, M, O, T — but an identifier alphabet that is a trivia question is not an improvement.) Second, for JMBG the compression barely buys anything: seven of the thirteen digits are a date you are supposed to read, which pins them to decimal forever. Re-encode the remaining six — region, serial, check — into base 32 and they fit in four characters: total saving, two characters, in exchange for a mixed-alphabet identifier that no longer fits any numeric-only field in any form or system it passes through. Croatia in 2008 is the more interesting case, because neither constraint applied: one script, no date to keep readable — and ISO 7064 even has an alphanumeric sibling, MOD 37,36, sitting in the same standard, ready to checksum a base-36 identifier. Eight Crockford characters would have worked. So why eleven digits? Because OIB was replacing a numeric identifier inside thirty years of infrastructure built around numeric identifiers — column types, field validations, paper forms, printouts, integrations — in a migration that was going to touch every register in the country. Changing the identifier’s meaning and its alphabet at the same time doubles the blast radius; keeping the shape and swapping only the semantics made OIB a near drop-in for any system that already handled a digit string. On top of that, an OIB gets read to a tax clerk over the phone every day, and ten digits survive that channel in any language. The prize for taking the risk: three characters.

Neither design’s trade is scandalous, and both come down to the same judgment call: when humans and legacy systems are the channel, characters are cheaper than rules.

What the semantics actually costs

One more experiment before letting this go. Re-encoding the whole number was crude — it dissolved the fields. Suppose we re-encode field by field instead, so the structure stays parseable by position:

day      1–31     1 char    a perfect fit — exactly 32 values
month    1–12     1 char
year              2 chars   1,024 years — the century ambiguity dies too
region            1 char    32 regions, redrawn
serial + sex      1–2 chars
check (mod 37)    1 char

Does a single serial character suffice? On averages, comfortably: the federation’s roughly one thousand births a day, spread over 32 regions, is about sixteen per sex per region per day — against a capacity of 32. But serial fields are sized for the pathological date, not the average one. The 1977 rollout assigned numbers retroactively — everyone alive got one, keyed to their own birth date — so a serial had to cover every living person in a region who shares a birthday, not one day’s newborns. And registries have a habit of defaulting unknown birth dates to January 1st, which quietly turns one date into a pileup. JMBG’s 500 per sex was headroom for exactly that tail. Field alignment also has a quieter cost here: decimal offered granularities of 10, 100, 1,000 — base 32 offers 32 or 1,024 and nothing in between. Need about a hundred? You pay for a thousand.

Add it up:

opaque serial (OIB-style)                     7 chars
semantic, 32 serials per day    (gambling)    7 chars    semantics: free
semantic, 1,024 serials per day (safe)        8 chars    semantics: 1 char
decimal JMBG                                 13 chars    semantics + glanceability

The entire JMBG semantics — date, region, sex, checksum — costs zero to one character over a fully opaque identifier. What costs five is the decimal alphabet: the property that a clerk reads a birth date straight off the card, with no decoder ring, no V meaning 27. The semantics is nearly free. The human eye is expensive. And the contract — the part the rest of this article is about — is the only price not measured in characters at all.

So should you store a JMBG as bigint? It fits. Please don’t.

Numbers that are not numbers

Identifiers that look numeric are not numbers. You will never add two JMBGs together. You will never compute the average OIB. The digits are notation, not quantity.

The practical giveaway is the leading zero. Everyone born in the first nine days of a month has a JMBG that starts with 0, and an OIB’s randomly chosen digits can start with 0 too. Store one as an integer and the zero is gone. Can you get it back? Sure — the length is fixed, so pad-left to 13 and the round-trip is lossless. But notice what you just did: the digit count no longer lives in the value; it lives in your code. Every consumer, forever, has to know the padding rule — and the consumers that don’t know it are exactly the generic tools that handle identifiers the most: the CSV export, the Excel import that eats leading zeros for breakfast, the JSON number type, the log formatter, the other team’s report. Each of them fails silently, producing a twelve-digit “JMBG” that still looks perfectly plausible.

An integer plus an out-of-band formatting rule is two pieces of state; a string is one. Add that you will never do arithmetic on these — sorting JMBGs numerically “sorts” people by day of birth, which is a bug wearing an index — and that a small value object can enforce the checksum at construction, and the honest conclusion is: char(13)/char(11) in the database, a value object in code, even though the value physically fits into a long. The five bytes you would save are not worth the contract you would be signing.

Compactness is still a real property — 13 characters beats 36 — but it is a property of the representation, not a license to do arithmetic.

It knows when you typed it wrong

The final digit of a JMBG is a check digit: a function of the other twelve, stored alongside them. Its job is to catch accidental damage — a mistyped digit, two digits swapped — at the point of entry, before the bad value reaches anything important. Label the digits a through l; then:

m = 11 − ( (7(a+g) + 6(b+h) + 5(c+i) + 4(d+j) + 3(e+k) + 2(f+l)) mod 11 )

K = m        for m = 1…9
K = 0        for m = 10 or 11

A detail I genuinely love: the 1976 federal law prescribes, in Article 6, that the control digit is determined “elektronskim računarom po modulu 11” — by electronic computer, modulo 11. The phrase dates the design more precisely than any archive photo could: this identifier was born at the exact moment administration met the computer, and the law felt the need to say so.

This is a weighted mod-11 scheme, a close relative of the checks in ISBN-10 and IBAN. Because every weight is non-zero mod 11, changing any single digit almost always changes the expected check digit, and because adjacent positions carry different weights, swapping two neighboring digits gets caught too. Almost all of it: the mapping of both m = 10 and m = 11 onto the same check digit 0 leaves a narrow blind spot where an error slides between those two cases undetected. A pragmatic 1970s trade — eleven possible remainders, ten available digits, and nobody wanted the Roman X that ISBN-10 used.

If “check digit” rings a bell from credit cards, that is Luhn — same job, different formula. Every serious human-transcribed identifier has one, because humans are a noisy transmission channel.

One distinction worth stating explicitly, because it applies to every API you will ever build: validation is not verification. A number that passes the checksum is structurally plausible. That is all. It does not prove the number was ever issued, that the person exists, or that the person presenting it is its owner. Our example 2902990711233 validates perfectly — for a birth date of 29 February 1990, a date that never happened. The checksum blesses an impossible person without blinking. A checksum protects you from typos, not from lies.

And reality cuts the other way too: a small number of officially issued JMBGs reportedly carry invalid check digits — clerical errors, especially from the early 1990s. Reject those on “validation” and you have just told a real person their state-issued identity is fake. The authoritative record wins over the checksum, in both directions.

This is excellent engineering

Under its original constraints, JMBG is close to optimal. Paper forms, typewriters, offices spread across six republics, no assumption of connectivity — and the design delivers decentralized issuance, offline attribute extraction, offline error detection, and human transcribability, in the smallest format that could plausibly hold all of it.

This is exactly the philosophy from Id Series #001, pushed further than any of us would dare push it in production: if the information is useful, put it directly in the identifier. Snowflake ids encode a timestamp and a worker number. Instagram ids encode a shard. JMBG encodes a person’s vital records.

Is that useful? Enormously — every consumer gets the attributes for free, forever, with zero lookups.

And that sentence should already be making you uncomfortable.

Croatia did almost the opposite

In May 2008 Croatia passed the Law on the Personal Identification Number, and from 1 January 2009 the Tax Administration of the Ministry of Finance began assigning the OIBOsobni identifikacijski broj — to every Croatian citizen and every legal person with a seat in Croatia. From 2010 it was mandatory in tax matters; from 1 January 2011, collecting JMBG became illegal in Croatia.

After the previous sections, the OIB’s format is almost comically disappointing:

XXXXXXXXXX K
│          └─ check digit (ISO 7064, MOD 11,10)
└──────────── ten randomly chosen digits

Ten random digits and a checksum. No birth date. No sex. No region. Eleven digits, and by deliberate design, ten of them tell you nothing.

That is not a limitation of the format. It was the point. The officially stated reasons for the switch were the informatization of public administration, unambiguous registration of persons as a precondition for data exchange between government systems — and, explicitly, that the OIB is determined randomly so that no personal data can be read from it, unlike the JMBG.

Croatia is, so far, the only successor state to replace the number outright — and nothing in EU membership forced it. EU law regulates how national identification numbers may be used, not how they are formatted, and half the Union happily runs semantic personal numbers with birth dates baked in — Sweden, Denmark, Poland, Czechia. Slovenia is the instructive case: an EU member since 2004, it kept EMŠO for the civil registers and simply added a separate eight-digit tax number — seven random digits plus a mod-11 check digit — for everything tax-shaped. Both philosophies, side by side, in one small country.

An identifier that knows nothing

The only computation an OIB consumer can perform is the structural check. The check digit follows ISO 7064 MOD 11,10, a “hybrid” scheme that alternates between modulus 10 and modulus 11 so it can detect all single-digit errors and adjacent transpositions while still emitting a plain decimal digit — no X, no blind spot from collapsing two remainders into one:

static int OibCheckDigit(string firstTenDigits)
{
    int a = 10;
    foreach (char c in firstTenDigits)
    {
        a = (a + (c - '0')) % 10;
        if (a == 0) a = 10;
        a = (a * 2) % 11;
    }
    int check = 11 - a;
    return check == 10 ? 0 : check;
}

The same ISO 7064 family guards IBANs and ORCID ids. So: an application holding an OIB can tell whether it is well-formed. To learn anything about the person behind it — name, birth date, anything — it must ask the authoritative government source, and that source can require authorization, log the access, and say no.

From a pure information-density perspective this is regression. Thirteen digits used to carry a whole record; eleven now carry nothing but identity. From a system-design perspective, that “waste” is the feature.

Information hiding, but for humans

Put the two architectures side by side and the comparison writes itself in ordinary software vocabulary:

JMBG                              OIB

identifier                        identifier
    ↓                                 ↓
decode (offline, by anyone)       authorized lookup
    ↓                                 ↓
birth date, sex, region           person record

                                  attributes

JMBG couples the identifier to a snapshot of attributes. OIB separates identity from attributes: the id is a stable key, the attributes live behind an interface. This is information hiding — the same principle you apply to a class’s private fields — applied to a national registry.

The consequences run in both directions, and they are the standard consequences of coupling:

  • Availability. JMBG’s attributes are readable when the network is down, the registry is down, or there is no registry. OIB’s attributes are readable exactly when the authoritative service is up and you are allowed in.
  • Access control. JMBG discloses to whoever sees the string — every log file, every invoice, every hotel reception desk. OIB discloses to whoever passes authorization at the source, where access can be controlled and audited.
  • Evolution. Attributes behind a lookup can be corrected or changed without touching the identifier. Attributes baked into the identifier are fixed at issuance — a wrongly recorded birth date, or a legal change of sex, collides with a “permanent” number that has the old value fused into it.
  • Disclosure by accident. With JMBG there is no such thing as sharing the identifier without sharing the attributes. They are the same string.

One nuance, so this doesn’t become a caricature: an opaque identifier is not automatically private. An OIB is still a persistent, unique, personal identifier — it is personal data, and it correlates records across systems just as effectively as JMBG does. The defensible claim is narrower and more interesting: an opaque identifier does not itself disclose the attributes of its owner. Possession of a JMBG string is knowledge of a birth date. Possession of an OIB string is knowledge of eleven digits. The correlation risk remains; the leakage-by-format is gone.

A short intermission

1977:

13 digits
→ identity
→ birth date, sex, region
→ error detection
→ works offline, on paper

2026:

UUID
→ HTTPS
→ OAuth
→ JWT
→ API gateway
→ three microservices
→ database

503 Identity Service Unavailable

Funny — and, taken straight, completely wrong. The 2026 stack is not incompetence; it is where the authorization, the auditing, the revocable access, and the independently changeable attributes physically live. The 1977 design has no 503 because it also has no access control: the read path cannot fail because there is no read path — everyone already has the data. You are not comparing a simple system to a bloated one. You are comparing a system that cannot say no to a system that can.

The ID is an API you cannot version

Here is the deepest lesson, and the one that reaches past national registries into everyday software.

A structured identifier is a tiny immutable API. The moment consumers learn that characters 1–7 are a date and characters 8–9 are a region, that layout is a contract. And people did learn it: for decades, forms, insurers, banks, and government systems across the former Yugoslavia have parsed sex out of the serial range and birth dates out of the prefix. Validators, spreadsheets, business rules — an entire ecosystem programs against the digit layout.

Now try to change it. You can’t version an identifier that is printed on fifty years of paper documents and burned into every database that ever stored a citizen. Croatia’s experience shows what “changing the format” actually costs: a new law, a new issuing authority, two years of parallel operation, rewriting every official register — land books, court registers, the securities depository — and finally making the old identifier illegal to collect. That is not a migration. That is a state-level strangler pattern, and it took the better part of a decade to finish.

This is the question Id Series #001 didn’t push on: embedding a timestamp or a shard id in your keys is cheap until the day that metadata becomes something you must not expose — and by then the encoding is load-bearing in systems you don’t control. Every semantic id is a schema commitment with no ALTER statement. And it is exactly the warning from Id Series #002, arrived at from the opposite direction: the reason you don’t expose meaningful identifiers is that exposure turns their meaning into a contract and a leak, simultaneously.

There is no contradiction between “ids can carry information” and “don’t expose meaningful ids.” The first is a design tool. The second is a boundary rule. JMBG is what happens when you use the tool and ignore the boundary.

Scorecard

No winner will be declared, because the two designs are not answering the same question:

PropertyJMBGOIB
Compactgood — 13 digits, 7 spent on the dategood — 11 digits, deliberately sparse
Alphabetdecimal only — script-neutral, dictation-safedecimal only
Human transcriptiongood, checksum-guardedgood, checksum-guarded
Offline attribute extractionexcellentnone — by design
Offline structural validationyes (mod 11, small blind spot)yes (ISO 7064 MOD 11,10)
Information disclosure by the string itselfhighlow
Coupling identifier ↔ attributeshighlow
Central lookup required for attributesnoyes
Attributes can change independently of the idno — encoded values are frozenyes
Decentralized issuanceyes, per regionno — central authority

JMBG optimizes for a world without infrastructure: every consumer self-serves, nothing depends on a central service, and the price is that the identifier tells everything to everyone, forever. OIB optimizes for a world made of infrastructure: attributes live behind an authorized, auditable interface, and the price is that nothing works without the registry.

The brilliant ID I would not build today

JMBG is the more interesting identifier — a genuinely elegant piece of 1970s data engineering that packed a record, a partition scheme, and error detection into thirteen digits. OIB is the better identity-system identifier — precisely because it refuses to be interesting.

Would I design something like JMBG today? Almost certainly not; its core strength, offline attribute extraction, solves a problem I no longer have, while its core weakness, disclosure fused into the format, is a problem I have everywhere. Does that make opaque ids universally superior? Also no — inside a system boundary, a Snowflake-style id that encodes time and shard is still a perfectly good tool, exactly as argued in #001.

The rule that reconciles the whole series is this: every bit you encode into an identifier becomes part of its contract. Encode information when the contract is genuinely useful, durable, and safe to honor for the identifier’s entire lifetime — which, for identifiers, tends to mean decades. Expose an opaque identifier when you want it to do exactly one job: identify.

Because everything an identifier knows, it will eventually tell.

Sources