Building Reliable LLM Applications in Java
Introduction
LLMs are usually associated with Python, but a great deal of production software — banking, enterprise backends, long-lived services — runs on the JVM, and those systems increasingly need to call language models too. Java’s strong typing and mature tooling are genuine assets here: they push you toward exactly the discipline reliable LLM applications require.
The core mindset is the same in any language: treat model output as a hypothesis to verify, not a fact to trust. This post covers the practices that make Java LLM applications production-grade, using Anthropic’s Claude and the official anthropic-java SDK.
Building Reliable LLM Applications in Python
Introduction
Calling an LLM API is easy. Building an application on top of one that is reliable — that fails predictably, doesn’t hallucinate its way into wrong answers, and doesn’t surprise you with a bill — is a real engineering discipline.
The core mindset shift: treat model output as a hypothesis to verify, not a fact to trust. This post covers the practices that make Python LLM applications production-grade, using Anthropic’s Claude and the official anthropic SDK.
Database Indexing and Query Optimization for Java Developers
Introduction
Fixing N+1 queries (see the previous post) gets your Hibernate app down to a handful of queries per request. The next bottleneck is what each of those queries costs once your tables have millions of rows — and that is almost always a question of indexing.
An index turns “scan every row” into “look it up directly.” Get the index wrong — or skip it — and a query that took 2ms in development takes 4 seconds in production once real data volume shows up.
Database Indexing and Query Optimization for Python Developers
Introduction
Fixing N+1 queries with select_related/prefetch_related or selectinload (see the previous post) gets you down to a small, sane number of queries per request. The next bottleneck is what each query costs once the table has millions of rows — and that is almost always about indexing.
An index turns “scan every row” into “look it up directly.” Skip it, and a query that’s instant in development takes seconds once real data volume shows up in production.
Designing for Change: Boundaries, Contracts, and Dependency Inversion in Java
Introduction
Most Java systems don’t fail because a single class was written badly. They fail because the boundaries between classes were drawn in the wrong place — a database library leaks into business logic, a payment provider’s SDK shows up in twelve unrelated files, and changing one third-party dependency means touching half the codebase.
Designing for change is not about predicting the future correctly. It’s about isolating what varies so that when it inevitably does vary — a new payment provider, a swapped database, a different message broker — the blast radius is one adapter, not the whole system.
Designing for Change: Boundaries, Contracts, and Dependency Inversion in Python
Introduction
Python’s flexibility makes it easy to wire everything together directly — call the requests library from inside your business logic, import the ORM model straight into your pricing rules, instantiate a third-party SDK client wherever it’s needed. It works, right up until the payment provider changes, or you want a fast unit test that doesn’t need a live database.
Designing for change means drawing boundaries around the parts of the system likely to move — third-party services, storage, transport — so that a swap or a test double touches one small adapter instead of rippling through the codebase.
Error Handling Best Practices in Java
Introduction
Error handling is where a lot of otherwise-good Java code quietly goes wrong. Exceptions get swallowed, null leaks across boundaries, and stack traces arrive with no context about what the program was trying to do.
Good error handling is not about catching everything. It is about being deliberate: fail fast on programmer errors, recover gracefully from expected failures, and never lose the information a future on-call engineer will need.
Error Handling Best Practices in Python
Introduction
Python’s error handling reads deceptively simply — try, except, done. But the difference between code that fails clearly and code that fails mysteriously comes down to a handful of habits: catching the right exception, preserving the original cause, and knowing when not to raise at all.
This post covers the practices that keep Python failures debuggable in production.
Prefer EAFP over Look-Before-You-Leap
Python idiom favors EAFP — “Easier to Ask Forgiveness than Permission.” Rather than checking whether an operation will succeed, attempt it and handle the failure:
Production-Grade Docker Images for Java
Introduction
A Java Dockerfile can be three lines and technically work — or it can be a 700 MB image that runs as root, rebuilds from scratch on every code change, and gets OOM-killed under load. The difference is a handful of well-understood practices.
This post walks through building Java images that are small, secure, cache-friendly, and container-aware.
Start From a Slim, Current JRE — Not a JDK
You need a JDK to build, but only a JRE to run. Shipping a full JDK bloats the image and widens the attack surface.
Production-Grade Docker Images for Python
Introduction
The typical first-draft Python Dockerfile — FROM python, COPY . ., pip install -r requirements.txt — produces an image that is close to a gigabyte, reinstalls every dependency whenever a single source file changes, and runs as root with a Python process that ignores SIGTERM.
This post covers how to fix all of that: small images, cached dependency installs, a non-root runtime, and clean shutdowns.
Pick the Right Base Image
python:3.12-slim is the pragmatic default: a Debian-based image with Python but without the large build toolchain. Avoid the full python:3.12 (hundreds of MB of tools you don’t run) and be cautious with alpine — its musl libc frequently breaks or slows down packages with C extensions (NumPy, pandas, database drivers), forcing slow source builds.