Most Java services I've looked at have at least one dependency that isn't really Java. It usually arrives as a wrapper: a JNI binding or a ProcessBuilder call around Ghostscript, a headless browser, ImageMagick or libvips. The Java side looks tidy, with a Maven coordinate and a sensible API. The part doing the actual work is a .so file or a binary on the PATH, and nobody on the team thinks of it as theirs.
That's fine right up until it isn't. In my experience it stops being fine at one of three moments, and none of them are when you'd choose.
The base image changes
Someone moves the service to a slimmer container image to cut startup time or image size. Alpine is the classic choice, and Alpine uses musl instead of glibc. A native library built against glibc either refuses to load or loads and then behaves strangely, and the error you get is an UnsatisfiedLinkError that tells you almost nothing about why.
The Java code didn't change and the JVM didn't care, but the binary underneath did.
The CPU architecture changes
A few years ago this was rare. Now half the developers on a team are on Apple Silicon laptops, and the cloud bill keeps nudging people toward ARM instances like AWS Graviton. The JDK has run on ARM for a long time, so pure Java code moves across without anyone noticing. A native dependency needs an ARM build to exist, to be packaged, and to be the same version as the x86 one you tested against. Sometimes all three are true. Often one isn't, and you find out in CI or, worse, on the first deploy.
Someone publishes a CVE
In September 2023, libwebp had a heap buffer overflow (CVE-2023-4863) that was being exploited in the wild. It was patched in Chrome quickly. It was patched much more slowly in everything else that had quietly bundled libwebp, which turned out to be a lot of things, including a lot of server-side image pipelines.
If your image handling goes through a native library, patching means knowing that library is there, knowing which version you shipped, and rebuilding whatever image or package contains it. If it's a Java library, patching is a version bump in your build file, and your existing dependency scanning probably flagged it already.
There's also a difference in how bad the bug is. A malformed file that triggers an out-of-bounds read in C is a potential remote code execution. The same malformed file in Java hits a bounds check and throws an exception. That's still a bug, and a crafted file can still cause a denial of service, but it's a much smaller category of bad day.
The licensing question nobody asked
Ghostscript is AGPL unless you buy a commercial license. That's a perfectly reasonable model, and Artifex is entitled to it, but I've seen it turn up in a dependency audit long after the service shipped, at which point the conversation involves lawyers instead of developers! Native tools tend to be pulled in by someone who needed it working by Friday, and license review doesn't always follow binaries the way it follows Maven dependencies.
What pure Java costs you
I'd be misrepresenting things if I said pure Java is free because it isn't!
Somebody has to implement the format. The PDF specification is enormous, image formats are full of vendor quirks, and a native library that's been hammered on for twenty years has handled edge cases a newer implementation may not have met yet.
Performance can also lag for some workloads, particularly where the native code leans on hand-tuned SIMD.
The JDK itself doesn't help as much as you'd hope. ImageIO dates back to Java 1.4, picked up TIFF support in Java 9, and still has no built-in support for WebP, AVIF or HEIC. It also has habits that waste an afternoon, like ImageIO.read returning null when it can't find a reader instead of throwing, or rejecting CMYK JPEGs that every other tool opens without complaint. So "just use the JDK" often isn't an option, and you end up choosing between a native wrapper and a third-party Java library anyway.
How to decide
Before adding a native dependency, I'd want answers to a few things. Who updates it when a CVE lands, and how would we find out? Does it have builds for every OS, libc and CPU architecture we run on, including developer laptops? What happens to the JVM if it segfaults? (The answer is that the whole process dies and leaves an hs_err_pid file behind, which is a worse failure mode than an exception you can catch.) What's the license, and has anyone outside engineering looked at it?
If those answers are all good, a native library will be the right call. Plenty of them are excellent. But the questions usually get asked after the first incident rather than before the first commit.
Disclosure: I work at a company that builds pure Java libraries for PDF and image processing, so I'm not neutral on this. The tradeoffs above apply regardless of whose library you use.