Skip to main content
  1. Posts/

Common Java Application Anti-patterns and Their Solutions

Throughout my work on various projects, I have consistently observed specific architectural issues that result in maintenance challenges.

The core problem stems from mixing different architectural layers, violating separation of concerns.

This leads to several issues:

  1. Rigid Architecture: Changes in one class cascade into multiple unrelated classes, creating a maintenance burden.

  2. Brittle Systems: Modifications in one component unexpectedly break functionality in seemingly unrelated parts.

  3. Poor Code Reusability: Components are tightly coupled, making code extraction and reuse impractical.

  4. Design Erosion: Developers tend to implement quick fixes rather than maintaining proper architecture due to the high cost of proper implementation.

  5. Unnecessary Complexity: Projects accumulate infrastructure code that doesn’t serve clear business purposes.

These issues typically arise from violating SOLID principles, particularly the Single Responsibility and Dependency Inversion principles.

Architectural Patterns for Well-Designed Applications #

Modern applications typically follow either layered (multi-tier) or hexagonal architecture patterns, depending on their complexity and integration requirements.

Layered Architecture #

Layered architecture is suitable for applications with limited external dependencies, consisting of:

  • Service Layer: Exposes external API endpoints
  • Business Layer: Implements core business logic and domain models
  • Integration or Data Access Layer: Handles external system integration aspects, such as persistence and external API calls

Here’s a biological analogy for the layered architecture:

layers.png
(AI generated image is illustrative)

  • Like the cell membrane, the Service Layer acts as the boundary - controlling what goes in and out through API endpoints.
  • The Business Layer represents the nucleus and cytoplasm - where core processes and business rules live, like how DNA contains the cell’s essential instructions.
  • The Integration Layer works like cell organelles (mitochondria, endoplasmic reticulum) - handling specialized functions and communications with the outside environment, similar to how these organelles process nutrients and exchange materials.

This structure isolates each layer’s responsibilities while allowing controlled interactions between them, just as cellular components have distinct roles but work together.

Hexagonal Architecture #

More complex applications with many integration points are better described by Hexagonal architecture.

It organizes the system by separating the internal structure (domain and application) from the external components (ports and adapters).

Hexagonal architecture
Hexagonal Application Architecture

Hexagonal architecture is better suited for complex applications with multiple integration points. It separates:

  • External parts: Ports defines protocols (interfaces) and Adapters (see GoF pattern) are implementation
  • Internal parts:
    • Domain is the central layer which contains all the business logic and business logic constraints. The domain layer responds in a technology independent way to whatever is being done in your architecture.
    • Application layer sits in between the domain and the framework and allows for communication between the two layers. Despite its name, this layer is not the actual application but serves to process commands received from the framework and relay them to the domain.

Think of hexagonal architecture like a eukaryotic cell with specialized proteins in its membrane:

hexagonal.png
(AI generated image is illustrative)

  • The outer ports/adapters are like membrane proteins (receptors and channels) that control specific interactions with the environment - each specialized for different types of communication
  • The inner domain core is like the nucleus containing DNA (core business rules), protected from direct external contact
  • The application layer acts like the nuclear membrane, regulating what signals can reach the core, similar to how nuclear pores control molecular traffic
  • External systems connect to specific “receptor” ports, just as hormones or nutrients bind to specific membrane proteins

Identifying Architectural Issues #

When components bypass their designated layers, it’s like cellular proteins appearing in the wrong locations - similar to when membrane proteins drift into the cytoplasm or nuclear proteins leak into other areas. This disrupts the cell’s carefully organized structure and causes dysfunction, just as misplaced code creates architectural problems. Like how a mitochondrial protein working in the cell membrane would break energy production, using data layer components directly in the presentation layer creates unstable, hard-to-maintain systems.

mixed.png
Yellow cells penetrating into blue cells (AI generated image is illustrative)

Key indicators of problematic design:

  1. The Presentation layer uses components or classes from the Integration layer, and vice versa. For instance, a JPA Entity is used directly in the REST API. This violates the Single Responsibility Principle: a change in the persistence layer could unexpectedly lead to changes in the external API, potentially breaking API clients or exposing sensitive data.

    Typical anti-pattern example
    // Anti-pattern: Mixing persistence and business logic
    @RestController
    @RequestMapping("/api/orders")
    public class OrderController {
        @Autowired
        private JpaRepository<Order, Long> orderRepository;
    
        @PostMapping
        public ResponseEntity<Order> createOrder(@RequestBody Order order) {
            // Business logic mixed with persistence and API concerns
            if (order.getAmount() > 1000) {
                order.setRequiresApproval(true);
                order.setStatus(OrderStatus.PENDING);
                // Direct exposure of persistence model as API response
                return ResponseEntity.ok(orderRepository.save(order));
            }
            return ResponseEntity.badRequest().build();
        }
    }
    

  2. Framework-dependent domain logic. Integration-specific details are leaked into business model, which is supposed to be technology-agnostic in order to support generalisation.

  3. Tightly coupled dependencies between different ports and adapters. One integration component depends on another integration component, breaking Dependency Inversion principle. Now replacing port implementation might be challenging due to dependencies.

An exception to these rules is when application is really-really tiny, e.g. single-purpose micro-service or utility.

As an example, lets’ consider this

Practical Refactoring Strategy #

Improving an existing application’s architecture is often more practical than a complete rewrite, which usually involves significant business risks, underestimated effort, and delays in feature development. This is especially true when the original developers are unavailable to provide context about past technical decisions and requirements. When the current system meets basic business and performance needs, gradual architectural improvements offer a pragmatic path forward that balances technical debt reduction with continued feature delivery, avoiding the common pitfall of maintaining parallel codebases during a rewrite.

When improving an existing application’s architecture, consider this approach:

1. Test Coverage #

  • Focus on functional and integration tests
  • Focus on high-level tests rather than unit tests at the start of refactoring.
  • Treat the system as a black box to preserve behavior

2. API Layer Separation #

  • Adopt a contract-first approach to API design.
  • Use OpenAPI/Swagger for REST services
  • Consider Protocol Buffers for high-performance requirements
  • Maintain separate models for external and internal representations

3. Integration Layer Isolation #

  • Maintain a clear separation between business models and integration models.
  • Implement dedicated persistence layer
  • Use adapters pattern for external service integration

Implementation Guidelines #

  • Start with small, incremental changes
  • Deploy frequently with proper monitoring
  • Implement feature flags for gradual rollout
  • Maintain backward compatibility during refactoring
  • Use continuous integration to catch integration issues early

Conclusion #

Sustaining a robust architecture demands ongoing effort and careful attention Regular refactoring and adherence to SOLID principles help in managing technical debt and system evolution.

Credits #