DEV Community

Sergiy Yevtushenko
Sergiy Yevtushenko

Posted on

We Should Write Java Code Differently: Less Language, More Business

How much of your code is actually about your business?

Open any Java service method. Count the lines. How many describe what the business does? And how many are null checks, exception handling, try-catch blocks, type conversions, logging boilerplate, and framework annotations?

In most codebases, the answer is uncomfortable. Technical ceremony dominates. Business logic hides between the scaffolding. A new developer reads the code and understands how it works — but not what it does or why.

This isn't a skill problem. It's a language problem. Java gives us powerful tools, but doesn't guide us toward using them in ways that preserve business meaning.

The Ratio

Consider a typical service method that processes an order:

  1. Validate the input (null checks, field validation, exception wrapping)
  2. Check inventory (try-catch around HTTP call, retry logic, timeout handling, response parsing)
  3. Calculate pricing (more HTTP, more try-catch, more parsing)
  4. Create the order (database call, transaction management, exception handling)
  5. Return the result (response mapping, error conversion)

Five business steps. But the code for each step is 80% technical handling and 20% business intent. The ratio is inverted — the scaffolding is louder than the signal.

Shrinking the Technical Part

What if the technical surface was standardized to the point where it almost disappeared?

Return types encode behavior. A method returning Result<Order> tells you it can fail — without looking at the implementation. Option<User> tells you the value might be absent. Promise<Response> tells you it's asynchronous. The type signature is the high-level documentation. No Javadoc needed to explain "this method might throw" — the return type already said it.

Composition operators have fixed semantics. flatMap means "if the previous step succeeded, do this next." all() means "these operations are independent — they have no ordering dependency and can execute in parallel." These aren't just API methods. They're business-level statements about relationships between operations. When you read all(checkInventory, calculatePricing), you know these two things don't depend on each other. That's domain knowledge encoded in structure.

Error types are exhaustive. A sealed interface listing every failure mode means a business analyst can read the error hierarchy and understand what can go wrong — without reading implementation code. The errors aren't strings or exception classes buried in catch blocks. They're first-class types that enumerate the business failure domain.

Patterns are structural, not creative. A Sequencer means "do these things in this order." Fork-Join means "do these things in parallel, combine results." A Leaf is a single operation with no sub-steps. The developer doesn't invent control flow — they select from a small set of patterns that map directly to how business processes work.

What Remains

When the technical part shrinks, what's left is business logic — and it becomes the dominant signal in the code.

The order processing method becomes:

static OrderService orderService(InventoryService inventory,
                                 PricingService pricing) {
    return request -> inventory.check(request.items())
                               .flatMap(pricing::calculate)
                               .map(OrderResult::placed);
}
Enter fullscreen mode Exit fullscreen mode

Three lines. Each line is a business step. The technical ceremony — HTTP calls, serialization, retry, error handling — exists, but it's handled by the runtime and the type system, not by the developer in this method.

Read it aloud: "Check inventory. Then calculate pricing. Then create the order." That's not a description of the code. That is the code.

Preserving Knowledge

This matters beyond aesthetics.

When technical ceremony dominates, a new team member reads the code and learns the framework. They understand how things are wired — which annotations trigger what, which configuration goes where, which exception handler catches what. This knowledge is framework-specific and doesn't transfer.

When business logic dominates, the same team member reads the code and learns the domain. They understand what the system does — which operations depend on each other, what can fail, what the valid states are. This knowledge survives framework migrations, team changes, and technology shifts.

The original developer's intent — the business reasoning behind the code — is preserved in the structure itself. Not in comments that drift from reality. Not in documentation that nobody updates. In the code.

The Language Shrinks

Something unexpected happens when you measure language features against business value: most of them become unnecessary.

Java is a large language. Inheritance hierarchies, checked exceptions, mutable state, reflection, annotation processing, type erasure workarounds, synchronized blocks, volatile fields — these are powerful tools. But when the goal is expressing business logic clearly, how many of them do you actually use?

The answer is surprisingly few. Records for data. Sealed interfaces for type-safe alternatives. Lambdas and method references for composition. Pattern matching for dispatch. That's most of it.

The rest — the features that generate conference talks and blog posts about clever techniques — serves the technical ceremony, not the business logic. Class inheritance exists to share implementation, not to model business concepts. Checked exceptions exist to force handling, but Result types handle errors more expressively. Mutable state exists for performance optimization, but immutable records are sufficient for business data.

This isn't a limitation. It's a feature. When the useful subset of the language is small:

  • The learning curve compresses. A new developer doesn't need to master all of Java — just the subset that carries business meaning.
  • Code becomes predictable. When there are three ways to express something, developers argue about style. When there's one way, they focus on the domain.
  • The "which feature should I use here?" decision disappears. The answer is always the same small set of constructs.

The implication is broader than one language. If Java's business-relevant subset is this small, adding more language features doesn't increase business expressiveness — it increases the technical surface that competes for attention with the business logic. Expressiveness comes from domain modeling, not from language syntax.

The Boundary Is Clear

The technical part of the code should be:

  • Small — a handful of types and patterns, not a framework vocabulary
  • Standardized — the same patterns everywhere, no per-developer style
  • Semantically meaningful — each construct maps to a business concept

The business part should be:

  • Dominant — more visible than the technical scaffolding
  • Readable — a sequence of named operations, not a tangle of callbacks
  • Exhaustive — every failure mode visible, every dependency declared

When these two conditions are met, code becomes what it should have been from the start: an executable specification of what the business does.


This is the sixth article in the "We Should Write Java Code Differently" series. Previous: The DI Confusion.

Top comments (0)