DEV Community

Lee
Lee

Posted on

What Production Java Taught Me: The Problems You Don't See in Tutorials

Java is easy to learn.

Building a Java application that remains reliable after millions of requests, growing datasets, changing requirements, and years of maintenance is a completely different problem.

After working on production systems with Java, Spring Boot, REST APIs, PostgreSQL, Redis, Docker, and cloud infrastructure, I've learned that the hardest problems usually aren't about syntax or frameworks.

They are about "making good engineering decisions under real constraints".

Here are some lessons that have stayed with me.

  1. Don't Optimize Java Before Understanding the System

One of the most common mistakes I see is trying to optimize code before identifying the actual bottleneck.

For example, developers may immediately focus on:

  • JVM tuning
  • object allocation
  • stream performance
  • garbage collection
  • replacing loops with streams

But the real problem might be a database query taking 800ms.

I've seen applications where changing a single SQL query or adding the right index produced a much larger improvement than hours of Java-level optimization.

My approach is usually:

Measure
  ↓
Find the bottleneck
  ↓
Understand why it exists
  ↓
Fix it
  ↓
Measure again
Enter fullscreen mode Exit fullscreen mode

Performance optimization without measurement is mostly guesswork.

  1. Clean Architecture Matters More as the Application Gets Older

A small Spring Boot application can survive with almost any structure.

A five-year-old application with hundreds of endpoints cannot.

Over time, business logic tends to leak into controllers, repositories, utility classes, and event handlers.

Eventually, changing one feature requires understanding ten unrelated parts of the system.

I try to keep responsibilities clear:

Controller
   ↓
Application / Service Layer
   ↓
Domain Logic
   ↓
Repository / External Services
Enter fullscreen mode Exit fullscreen mode

The exact architecture isn't the important part.

The important part is knowing "where a responsibility belongs".

Good architecture should make the next change easier—not simply make the current code look impressive.

  1. Database Performance Is Part of Backend Engineering

A Java developer working on backend systems cannot treat the database as someone else's problem.

Consider:

SELECT *
FROM orders
WHERE customer_id = ?
ORDER BY created_at DESC;
Enter fullscreen mode Exit fullscreen mode

If this query runs frequently against millions of records, the right index can matter enormously.

For example:

CREATE INDEX idx_orders_customer_created
ON orders(customer_id, created_at DESC);
Enter fullscreen mode Exit fullscreen mode

But indexes aren't automatically good.

They increase storage requirements and can make writes more expensive.

The engineering question isn't:

"Should I add an index?"

It's:

"What access pattern does this system actually need?"

That distinction becomes increasingly important as traffic and data volume grow.

  1. Distributed Systems Change Everything

A method call is predictable.

A network call isn't.

When your Java application depends on another service, you have to assume that the dependency can:

  • become slow
  • return errors
  • time out
  • return unexpected data
  • become temporarily unavailable
  • recover while your application is still processing a previous request

That's why production integrations need things such as:

Timeouts
Retries
Circuit breakers
Idempotency
Rate limiting
Observability
Enter fullscreen mode Exit fullscreen mode

One particularly important lesson is that "retries are not always safe".

Retrying a GET request is very different from blindly retrying a payment or order-creation request.

Before adding retries, I ask:

"What happens if the first request actually succeeded but the response was lost?"

That's where idempotency becomes critical.

  1. Concurrency Bugs Are Usually Harder Than Compilation Errors

Java gives us powerful concurrency primitives, but production concurrency problems can be extremely subtle.

A race condition may happen once every few thousand requests.

That makes it difficult to reproduce locally.

For example, shared mutable state can create problems even when individual operations look correct.

Instead of asking:

"Is this method thread-safe?"

I prefer asking:

"What state is shared, who owns it, and how can multiple requests modify it?"

Reducing shared mutable state is often more valuable than simply adding synchronization everywhere.

  1. Observability Is Part of the Feature

Logs are not just for debugging after something breaks.

A production service should help us answer:

  • What happened?
  • When did it happen?
  • Which request caused it?
  • Which dependency failed?
  • How long did it take?
  • How many users were affected?

For important operations, structured logging and metrics are incredibly valuable.

For example:

request_id
customer_id
operation
duration_ms
status
dependency
Enter fullscreen mode Exit fullscreen mode

When something goes wrong at 2 AM, observability can be the difference between a five-minute investigation and a two-hour investigation.

  1. Don't Hide Business Logic Behind Frameworks

Spring is powerful.

But Spring shouldn't become the architecture.

I've seen codebases where developers rely so heavily on annotations and framework behavior that it becomes difficult to understand what the application actually does.

Frameworks should support business logic.

They shouldn't define it.

A good test is:

If I removed the framework annotations, could I still explain the important business rules?

If the answer is no, there may be too much framework coupling.

  1. Tests Should Protect Behavior, Not Implementation Details

I don't believe every line needs a unit test.

I care more about whether the important behavior is protected.

For example:

Given an expired subscription
When the customer requests a premium operation
Then the operation must be rejected
Enter fullscreen mode Exit fullscreen mode

That's a business rule.

A good test protects that rule even if the internal implementation changes.

This also makes refactoring much safer.

  1. Simplicity Is an Engineering Skill

Senior engineers aren't valuable because they can write complicated code.

They're valuable because they can recognize when complexity isn't necessary.

I've learned to question things like:

  • Do we really need another service?
  • Do we need asynchronous processing here?
  • Does this need a distributed cache?
  • Do we need a custom framework?
  • Is this abstraction solving a real problem?
  • Will the next developer understand this six months from now?

Sometimes the best architecture is simply the boring one.

And boring systems are often the systems that are easiest to operate.

Final Thoughts

After years of working with Java, my biggest lesson isn't about Java itself.

It's this:

"Production engineering is mostly about managing complexity."

Java gives us excellent tools for building large systems, but the language and framework can't make architectural decisions for us.

The real skill is knowing when to optimize, when to simplify, when to introduce abstraction, when to accept complexity, and—perhaps most importantly—when "not" to add another layer.

The best production code I've worked on isn't necessarily the most clever code.

It's the code that another engineer can understand, monitor, test, change, and safely deploy years later.

Top comments (0)