Blog
Technical writing on software architecture, distributed systems, AI engineering, Java, and building reliable software at scale.
AI tools make it possible to build a working app without writing a line of code yourself. Getting that app to survive real users, real data, and real attackers is a different problem, and it has a different solution.
The branching strategy debate is not really about branches. It's about how frequently you integrate, how you manage risk, and whether your team has the engineering practices to support continuous deployment.
Code that demonstrates technical sophistication at the cost of readability is not good code — it's expensive code. The people who pay are the engineers who maintain it.
APIs that require callers to opt into security are APIs that will eventually be used insecurely. The better design principle: secure behavior should be the default, and unsafe behavior should require explicit effort.
Software that can't be changed without fear is expensive software. Design choices that bound the impact of changes — stable interfaces, clear boundaries, limited assumptions — determine how much change costs.
Terraform, Pulumi, and CloudFormation templates are code. They have the same quality problems as application code — and require the same engineering practices to manage well.
Two fundamentally different philosophies for managing production infrastructure. Immutable infrastructure replaces; configuration management updates. Understanding the trade-offs determines which fits your operational context.
Architecture reviews are useful only when they challenge assumptions rather than validate preferences. A practical framework for evaluating architectural proposals on the dimensions that actually matter.
AI features introduce attack surfaces that traditional application security doesn't cover. Prompt injection, data leakage, excessive agency, and insecure RAG are real production risks.
AI features introduce new dependencies, new operational concerns, and new failure modes. Keeping them properly bounded prevents vendor lock-in, cost surprises, and the slow erosion of your system's architectural coherence.
AI agents that work in demos often fail in production. Tool use, state management, error handling, observability, and cost control — what actually matters when you're shipping agents.
Production AI systems have the same requirements as any other distributed system: testing, observability, security, reliability, versioning, and cost management. Treating AI as special leads to systems that fail in predictable ways.
The Model Context Protocol standardizes how LLMs connect to tools, data sources, and external systems. Here's what it actually is, how it works, and when a plain API is still better.
Multiple agents don't automatically produce better results. Understanding when agent coordination adds value — and when it's just distributed chaos — is the question worth asking.
You can't improve what you don't measure. LLM application quality requires systematic evaluation — golden datasets, LLM-as-judge, retrieval metrics, and regression testing across model and prompt versions.
Both RAG and fine-tuning can make an LLM more useful for your domain. They solve different problems. Choosing between them — or combining them — requires understanding what each actually does.
Database schema changes are the hardest part of continuous deployment. Zero-downtime migrations, expand/contract patterns, backward compatibility, and why your deployment strategy is only as good as your migration strategy.
Spring's DI container is powerful. It's also easy to let it drive your architecture rather than support it — component scanning everywhere, field injection, circular dependencies, and configuration that's impossible to test.
Senior engineers reviewing pull requests should spend their time on correctness, API design, failure modes, and architectural impact — not indentation and naming conventions that a linter should catch.
Java performance work without profiling data is expensive guesswork. JFR, async-profiler, GC tuning, and allocation analysis — the tools and mental model for finding real bottlenecks.
Resilience patterns — timeouts, retries, circuit breakers, bulkheads — are not features to add everywhere. They're mitigations for specific failure modes. Applying them deliberately changes outcomes.
Security that's added after the architecture is designed is a patch. Security that's part of the architecture is a property. The difference shows up in incident response.
Each deployment strategy makes different trade-offs around risk, infrastructure cost, rollback speed, and operational complexity. Choosing the right one requires understanding what you're optimizing for.
Structured concurrency, in preview since Java 21 and still evolving as of Java 25, brings the same lifecycle discipline to concurrent tasks that try-with-resources brought to resource management. Here's what it solves and how to use it.
Terraform is straightforward for simple infrastructure. At scale, state management, module design, and drift detection are where the real complexity lives.
Every abstraction adds complexity. The question is whether the benefit — hiding detail, enabling change, reducing duplication — justifies that cost. Usually it's worth asking before adding the abstraction.
Spring Boot 3.x has excellent observability support built-in. But instrumenting your application is not the same as having useful observability. Here's the difference.
Authentication and authorization are distinct problems with distinct solutions. Mixing them up produces security architectures that are hard to reason about and easy to get wrong.
Technical debt is often discussed as a problem to eliminate. The reality is more nuanced — some debt is intentional, strategic, and worth carrying. The question is which debt is which.
The Spring Boot testing ecosystem is extensive. Knowing when to use @SpringBootTest vs @WebMvcTest vs plain unit tests, and understanding the cost of each, is what makes a test suite useful.
The transition from senior to staff engineer is not about technical excellence. It's about changing where your engineering judgment gets applied — from your own code to the systems, teams, and decisions around you.
Records and sealed classes aren't just syntactic sugar. Used together, they enable domain models that are more correct, more readable, and harder to misuse than traditional Java beans.
Pattern matching in Java 21 — from simple instanceof patterns to record destructuring and exhaustive sealed type switches — changes how you write and reason about domain code.
Clean code is a widely misunderstood idea. It's not about short methods, specific naming conventions, or following Uncle Bob's style guide. It's about managing complexity.
The microservices vs monolith debate is usually framed wrong. The real question is: what architecture reflects your organizational structure and operational maturity?
Events decouple producers from consumers and enable powerful async patterns. They also introduce ordering problems, eventual consistency, and operational complexity that synchronous systems don't have.
The combination of records, sealed classes, pattern matching, and virtual threads that landed between Java 17 and 21 fundamentally changed what idiomatic Java looks like.
The classic controller-service-repository layered architecture works for simple applications. As complexity grows, it doesn't. Here's what a well-structured Spring Boot application actually looks like.
The value of CI/CD isn't automating deployments. It's compressing the time between writing code and knowing whether it works. Most implementations miss this.
ADRs are one of the highest-leverage engineering practices most teams don't do. Here's what they are, why rejected alternatives matter, and a template that works.
Project Loom's virtual threads landed in Java 21 as a production feature. They fundamentally change the scalability model of blocking Java applications — with important caveats.
Building systems that communicate over a network introduces a class of problems that don't exist in single-process applications. Understanding them is prerequisite to designing around them.
Retrieval-Augmented Generation is a retrieval problem as much as a generation problem. Most teams underinvest in the retrieval half and then blame the LLM.
Microservices get all the attention, but modular monoliths solve the real problem — enforced module boundaries — without the operational overhead of distributed systems.
A practical guide to the Java language improvements from Java 9 through Java 17 — the features that change how you model and write code, not just syntactic sugar.