ENUM and EIR are two services no end user will ever hear of, yet every call to a ported number and every device registration on a mobile network passes through one of them. We built both for a Polish mobile operator as part of its core-network infrastructure — the clients here are core-network elements (MME, SIP/IMS nodes), not people holding phones. When a service like this goes down, calls to ported numbers land at the wrong operator, or the MME has no way of knowing whether to let a device onto the network. So we designed the whole architecture around one question: what happens when something other than the service itself fails.
What ENUM and EIR actually do
ENUM is a DNS server answering NAPTR queries in the e164.arpa domain — a gateway to NPDB, the number-portability database. Given an MSISDN, it returns a Routing Number and maps it to the SIP domain of the operator the number was ported to (mobile number portability). EIR — we call it DEIR internally, since it’s the Diameter variant — receives 3GPP S13 requests (the ME-Identity-Check command, command code 324) from the MME and answers with an IMEI status: whitelist, blacklist, or greylist, backed by LDAP. Target throughput: 3,000 TPS for ENUM, 4,000 TPS for EIR, across two sites at once.
Cache as a safety net, not just an accelerator
Both services face the same problem: the LDAP behind NPDB or EIR can’t become a single point of failure for signaling traffic. The answer is Hazelcast, embedded as an in-memory distributed map on every instance (TCP/IP discovery, 10-minute TTL, one backup). It plays two roles at once: at peak TPS it takes load off LDAP, since most repeat lookups for the same number or IMEI hit the cache; and when LDAP is unreachable, the service keeps answering — with the last known value instead of a timeout. The cache also exposes a REST endpoint to toggle it on and off and inspect its size, which in practice means we can disable it on production without a restart when we need to debug whether a problem sits in the cache or in LDAP.
That’s a deliberate trade-off: an embedded Hazelcast cluster without an external server is simpler to run, but weaker on cross-site cache consistency. Site A and site B each keep their own copy — if a number is ported and NPDB is updated, each site finds out with its own TTL-bound delay. For a read-heavy service whose underlying data changes far less often than it’s read, we judged that acceptable.
Takeaway. A cache in a critical system has to be designed around the failure of its data source, not just its load — that’s a different set of requirements entirely.
Two sites, one service
Georedundancy doesn’t stop at two service instances behind a load balancer. Eureka, our service discovery, runs across two sites — site A and site B profiles instead of one cluster stretched across locations. Configuration comes from a Spring Cloud Config Server with its own admin panel and a PostgreSQL backend, parameterized per site and per machine.
The weak point of this model is the config server itself — if it goes down right as a service process happens to restart, the service ends up with no configuration at all. So we added a FallbackConfigRefreshListener: when the Config Server stops answering, the service swaps its property source for a local file holding the last known good configuration. It’s the same pattern as Hazelcast-as-fallback, just applied to configuration instead of business data.
Reviving a 12-year-old Diameter library
EIR needs a working Diameter stack in the JVM — peer connections, sessions, an AVP dictionary, 3GPP S13 support. The natural choice was RestComm jDiameter (formerly Mobicents), but it’s a library from around 2013, formally handed over to Mobius Software in 2023 and practically unmaintained — hard to get in the versions we needed even from Maven Central. The alternative was a commercial Diameter stack, but that’s a different cost and a different integration timeline, out of proportion to the size of the project.
We decided to vendor the entire library into our repository — the core, ha, and mux modules — build it locally and install it into a local Maven repo. We also extended the AVP dictionary (dictionary.xml) to nearly 7,900 lines: the standard 3GPP applications (Rf, Ro, Cx/Dx, Sh, Gx, Rx, S6a) plus our own AVPs for S13 — Terminal-Information (1401), IMEI (1402), Equipment-Status (1445).
public class MeIdentityCheckHandler implements NetworkReqListener {
@Override
public Answer processRequest(Request request) {
if (request.getCommandCode() != 324) { // ME-Identity-Check
return null;
}
AvpSet avps = request.getAvps();
String imei = avps.getAvp(1402).getUTF8String(); // IMEI, e.g. 490154203237518
EquipmentStatus status = eirLookupService.lookup(imei); // LDAP, Hazelcast fallback
Answer answer = request.createAnswer(ResultCode.DIAMETER_SUCCESS);
answer.getAvps().addAvp(1445, status.getCode(), VENDOR_ID_3GPP, true, false);
return answer;
}
}Maintaining a fork of a legacy open-source library is a real cost — no upstream fixes are coming from anyone else. But it was the only realistic path to a working S13 server in Java without signing a contract for a commercial Diameter stack.
Spring Boot or Quarkus for S13
Alongside the production deir-service (Spring Boot), we built a prototype of the same functionality on Quarkus, using the io.quarkiverse.jdiameter extension. The difference in code ergonomics is clear. In Spring Boot you check the command code yourself, pull raw AVPs by number, and assemble the answer AVP by AVP. In Quarkus, a specialized, typed API is available: ServerS13SessionListener, JMEIdentityCheckRequest/Answer, S13SessionFactoryImpl — the domain code reads cleaner and is less prone to an AVP-number typo.
Even so, production shipped on Spring Boot. The rest of the monorepo — Spring Cloud Config, Eureka, the Hazelcast-spring integration — was already there, so ecosystem consistency won over a single component’s better developer experience. The Quarkus prototype never got full LDAP and cache integration and stayed an experiment.