Protecting Your Spring Boot App from ML Framework Churn

Hero image for Protecting Your Spring Boot App from ML Framework Churn
Architecture diagram generated by [Google Gemini](https://ai.google.dev)

Introduction

Machine learning frameworks change fast. One month, TensorFlow is the standard. The next, everyone’s switching to PyTorch, or a new hot library appears. If your Spring Boot application has ML code tangled directly into its core business logic, every framework change becomes a rewrite disaster. This tutorial teaches you exactly how to prevent that. You’ll learn four concepts that work together to protect your application: Hexagonal Architecture, Decoupling, Spring, Isolation, and Architecture itself. We’ll define each one, show how it works, and give you a concrete code example you can use today. By the end, you’ll have a practical strategy to keep your core business logic clean and safe from ML framework volatility.

What Is Hexagonal Architecture? (And Why It’s Not a UFO)

Let’s start with the strangest-sounding term: Hexagonal Architecture. In plain English, it’s a way of designing software where your core business logic sits in the middle, isolated from the outside world (databases, web layers, ML models). The name comes from the hexagon shape, but that’s just a visual metaphor—the real point is the separation, not the geometry.

How it works: The core of your application defines ports (interfaces) for what it needs to do. Outside components (called adapters) implement those interfaces. The core never directly calls an ML library or a database. It only talks through the ports.

Real-world analogy: Think of a power outlet. Your lamp (core business logic) has a standard plug (port). The power plant (ML framework) can be coal, nuclear, or solar—it doesn’t matter. As long as the adapter (the wall socket) matches the plug, your lamp works. If you change power sources, you just swap the adapter, not the lamp.

Code example: Here’s a simple port for an ML prediction service in a Spring Boot app:

// Port: defines what the core needs
public interface PredictionService {
    PredictionResult predict(InputData data);
}

// Adapter 1: uses TensorFlow
@Component
public class TensorFlowPredictionAdapter implements PredictionService {
    @Override
    public PredictionResult predict(InputData data) {
        // TensorFlow-specific code
    }
}

// Adapter 2: uses PyTorch (swap without touching core)
@Component
public class PyTorchPredictionAdapter implements PredictionService {
    @Override
    public PredictionResult predict(InputData data) {
        // PyTorch-specific code
    }
}

The core business logic only depends on PredictionService, not on TensorFlow or PyTorch directly. Non-obvious gotcha: Ports should be business-centric, not framework-centric. If your port method returns a TensorFlow tensor, you’ve already leaked framework details—keep return types as plain Java objects.

Decoupling: The Art of Not Being Tied Down

Decoupling means reducing the dependencies between different parts of your system. When two components are tightly coupled, changing one forces changes in the other. Decoupling lets each part evolve independently.

How it works: Decoupling happens at multiple levels—through interfaces, dependency injection, and message passing. The goal is that component A can be swapped or modified without understanding the internal details of component B.

Real-world analogy: Consider a music streaming service. Your phone (core app) doesn’t need to know that your Bluetooth speaker (adapter) uses a specific codec. They’re decoupled by the Bluetooth protocol (port). If you upgrade the speaker, your phone still works. That’s decoupling in action.

Code example: In our Spring Boot app, the core business logic is decoupled from the ML framework through the PredictionService interface. If we update TensorFlow’s API, the core remains unchanged. But there’s a trap: implicit coupling through data structures. Even if you use an interface, if your core receives a TensorFlowData object, you’re still coupled. Always define shared data models that are framework-agnostic.

How Spring Makes This Easy

Spring is a Java framework that provides dependency injection, inversion of control, and a container for managing objects (beans). It’s the glue that ties your hexagonal architecture together without code tangles.

How it works: Spring scans your codebase for @Component, @Service, @Repository, and other annotations. It then creates and wires those beans automatically. You tell Spring what you want (the interface) and Spring injects the correct implementation at runtime.

Real-world analogy: Spring is like a very organized restaurant manager. You (the core logic) don’t need to know which chef (adapter) will prepare the pasta (ML prediction). You just place your order through a menu (port). The manager (Spring) ensures the right chef gets the order and your plate arrives correctly.

Code example: Using Spring’s dependency injection to switch between ML adapters:

@Service
public class BusinessService {
    
    private final PredictionService predictionService;
    
    // Spring injects whichever @Component implements PredictionService
    public BusinessService(PredictionService predictionService) {
        this.predictionService = predictionService;
    }
    
    public void process(InputData data) {
        PredictionResult result = predictionService.predict(data);
        // business logic here
    }
}

Non-obvious insight: Spring profiles (@Profile("tensorflow"), @Profile("pytorch")) let you switch implementations in different environments. Test with a mock adapter (@Profile("test")), but never use profiles to paper over design flaws—the architecture should be clean regardless of which adapter loads.

Isolation: The Fortress for Your Core

Isolation means that the core business logic doesn’t know or care about external frameworks. It’s completely shielded from changes in those frameworks. This is the ultimate goal of hexagonal architecture.

How it works: Isolation is enforced at compile time through interfaces and at runtime through classpath management. The core module shouldn’t even have the ML framework’s JAR on its classpath. It only sees the port interfaces and plain Java objects.

Real-world analogy: A bank vault’s control room (core logic) doesn’t need to know that the vault door (adapter) is made by a specific manufacturer. As long as the door responds to “open” and “close” commands (port), the control room stays isolated from any vendor lock-in.

Code example: A Maven module structure that enforces isolation:

my-app/
  core/          # No ML framework dependency
  ml-adapter/    # Depends on core + TensorFlow
  web-adapter/   # Depends on core + Spring MVC
  rest-api/      # Depends on core + both adapters

The core module’s pom.xml has no reference to TensorFlow, PyTorch, or any ML library. The ml-adapter module does. If you later remove that module, the core compiles and runs fine (minus the ML feature, of course). Gotcha: Injection of adapter dependencies via Spring can still leak if you’re not careful—use @Qualifier to be explicit about which adapter to inject.

Architecture: The Blueprint That Connects Everything

Architecture is the high-level structure of your software—the decisions that are hard to change later. Hexagonal architecture is one pattern; decoupling is a principle; Spring is the tool; isolation is the result. Together, they form a coherent architecture that adapts to change.

How it works: Architecture dictates the boundaries between components, the rules for communication, and the points of extension. In our case, the architecture says: core is always on the inside, adapters are always on the outside, and ports define the contracts.

Real-world analogy: A city’s architecture determines where roads, parks, and buildings go. Decoupling is like having wide roads that allow different building styles. Spring is the public transit system. Isolation is the zoning laws that separate heavy industry from residential areas.

Comparison Table:

Concept What It Does Real-World Analogy Spring Tool
Hexagonal Architecture Defines the overall structure with ports and adapters Power outlet and plug Interface + @Component
Decoupling Reduces dependencies between components Bluetooth protocol Dependency injection
Spring Manages and wires components Restaurant manager @Autowired, @Bean
Isolation Shields core from external changes Bank vault control room Module separation in Maven/Gradle

Key Takeaways

  • Hexagonal Architecture separates core logic from infrastructure using ports and adapters.
  • Decoupling means components don’t depend on each other’s internal details—use interfaces.
  • Spring provides dependency injection to wire adapters into your core without tight coupling.
  • Isolation ensures your core module has no dependency on external ML frameworks.
  • Architecture is the blueprint that combines all these concepts into a coherent, maintainable system.

Your Spring Boot app’s core can survive any ML framework change—as long as you build the wall first. Now go decouple something.