Posted on ::

I dislike Java.

The verbosity, the AbstractFactoryPatternManagerImpl nonsense. Every time I write Java I think it would be less verbose to write another blog post.

Zig and Rust are right there, looking at me, constantly. Beautiful systems languages. Manual memory management. Zero-cost abstractions. No garbage collection pauses. I could write blazingly fast code that runs circles around the JVM.

So naturally, when working on a Java microservice, I started to think how can I justify incorporating a system level language like those two into this ecosystem.

Spoiler: I could not. But why I could not exemplifies the difference of the real world performance and constraints over theoretical performance.

The sexy proposition

Systems languages promise real performance. Not "well-tuned JVM" performance. Not "good enough for CRUD" performance. Actual bare-metal, talk-to-the-kernel, control-every-byte performance.

And when you're maintaining a service that feels slow and you are like me (you don't shy away from learning something new as long as it makes sense), that appeal is intoxicating. You start manufacturing opportunities everywhere:

  • "This XML parser could be 3x faster in Zig!"
  • "These protobuf codecs are doing unnecessary allocations!"
  • "We could rewrite the database layer and eliminate JNI overhead!"

Sadly, it is not that simple unless you are reckless. You need to do the fucking math first., and that is not even accounting for the hard sell of adopting a new technology that you will have to maintain. New pipelines, new monitoring, a lot of new things.

The case study: A Java Microservice

Let me tell you about a real service. Distributed user data service. Java/JVM stack. gRPC with some REST API sprinkles, database operations, message queues. Handles tens of thousands of requests per second.

The performance profile:

  • I/O-bound (network, database, external APIs)
  • Latency targets: tens to hundreds of milliseconds
  • Already using async patterns

I started investigating where we could drop in some Rust or Zig. Found three candidates:

1. XML/TTML Parser

  • Parsing files
  • Could be 2-3x faster in native code
  • Absolute improvement: ~5-10ms
  • Not on critical path

2. Protocol Buffer Codecs

  • 400+ lines of serialization logic
  • Already using binary protobuf (efficient)
  • Potential gains: <5ms vs 10-100ms network latency

3. Database Access Layer

  • This is where it gets interesting

The sad truth

Here's the thing about this service: it uses a distributed database that's already written in C++. Every database operation goes through this flow:

Java Service Layer
      ↓ (JNI - ~100-200ns overhead)
JVM Bindings (Java wrapper)
      ↓
Native C Library
      ↓ (Network - ~1-5ms latency)
Database Server (C++)

You know what I thought? "If we wrote the data access layer in Rust, we could talk directly to that C library! We'd eliminate the JNI overhead and I for sure can make it safer" (the latter one is of course delusional)

So I did the math.

The math

Single-Key Lookups (most common operation):

  • Current latency: 1-5ms
  • JNI overhead: 100-200ns
  • JNI as percentage of total: 0.01-0.02%

Range Queries (10-100 items):

  • Current latency: 5-20ms
  • JNI overhead: 100-200ns
  • JNI as percentage of total: 0.001-0.004%

Even if I made JNI infinitely fast, I'd improve overall latency by less than 2%.

Because the network latency to the database (1-5ms) is 5,000 to 50,000 times larger than the JNI overhead.

Two orders of magnitude or don't mention it

Here's my new rule: Only optimize a component if it's within two orders of magnitude of your dominant cost.

  • JNI overhead: 100-200ns
  • Network latency: 1-5ms (5,000-50,000ns)
  • Difference: 3-4 orders of magnitude

Not. Worth. Touching.

This applies to everything. Why would I ever start by optimising anything other than the bottleneck...

When yes?

Look, I'm not saying systems languages are useless for web services. They absolutely shine when you have:

CPU-intensive operations at massive scale

  • Millions of ops/sec, not thousands
  • Computational kernels called constantly (crypto, compression, encoding)
  • Data processing pipelines without I/O waits

Memory-sensitive workloads

  • GC pauses directly impact user experience
  • Sub-millisecond latency requirements (<1ms p99)
  • Every microsecond actually matters

You may be able to build a case if you already have a custom proxy server, cache layer, in memory analytics engine or stuff like video encoding I guess.

Noticed something? They're not waiting on I/O. They're CPU-bound or have latency requirements where microseconds actually matter.

For a typical CRUD service hitting databases and external APIs? You're I/O-bound. The language doesn't matter.

The sadder truth

99% of developers (including me) don't have problems that systems languages solve.

Your web service is slow because your database queries suck, or because you are not caching properly. You are underutilizing async. If I had a penny for every time I encountered multiple awaits in line one after the other rather than await all, I would have like 2 dollars more in my bank account.

Your web service is NOT slow because Java has garbage collection, it is slow either because of layer 8, or bound to slow I/O.

Imagine you are in that 1% where you need it

Okay, let's say all the planets have aligned. You've profiled, you've done the math, and introducing a systems language actually makes sense. You have CPU-bound operations at massive scale. The 10x improvement is real.

Systems languages are fast, but they're not magic. You still need to put in the work to make it fast and not shoot yourself in the foot. Rust won't save you from terrible algorithms. Zig won't fix your O(n²) nested loops. And both will happily let you write slow code if you're not careful. (btw, if this seems scary, that means you current code could use a revision ;) )

And then there's still the organizational battle. You need to fight for adopting something entirely new within a larger organisation. Onboarding a new language is never as easy as installing the compiler and creating a ci/cd pipeline.

You will have to worry about split language expertise and codebase. Hiring becomes harder. Context switching is annoying, or you end up with knowledge silos within the team.

You may not struggle with platform specific binaries, but what about libraries? Does the obscure SaaS your company forces you to use for performance metrics have a good library for your new Rust server? Or will you have to build it yourself to get the minimum amount of observability in prod that will allow you to actually measure if all this was worth it?

I would still probably say "only introduce platform complexity if it gives you 10x improvement", or something like that

For this I/O-bound service, Rust might give 1.1-1.2x on the compute portions. Compute is <10% of request time. Overall improvement: <2%.

Not worth doubling operational complexity.

My reflection

I wanted to use Zig and Rust because they're cool. Because systems programming feels like "real" engineering. Because I was bored of Java.

Those are preferences, not requirements.

The discipline is doing the math first. Profiling before rewriting. Asking "what's actually slow?" instead of "what could be faster?"

Network latency doesn't care what language you use. Database transactions take the same time whether you call them from Java or Rust. External APIs respond just as slowly to your beautiful systems programming code.

Choose boring, proven technology that matches your problem domain. Save your systems programming energy for a toy project. You will enjoy it more and it won't be tainted by your job requirements.

Table of Contents