FastAPI for internal automations: when to choose Python over Spring Boot

When it makes sense to use FastAPI for internal tools and when Spring Boot is the better choice. Real technical criteria.

Cover for FastAPI for internal automations: when to choose Python over Spring Boot
Updated: May 18, 2026

A couple of years ago I was asked to build an internal tool so the data team could trigger scraping jobs on demand. I needed a simple REST API, integration with Python scripts that already existed, and a minimal panel to view execution status. My first impulse was to set up a Spring Boot project. My second impulse, after thinking for ten minutes, was to open a main.py file and have an endpoint running in twenty lines with FastAPI.

That decision saved me days of work. But it’s not always like that. I’ve had other cases where I started with FastAPI and ended up wishing I had Spring Boot’s structure. The difference between a good and a bad technical decision here isn’t which framework is “better,” but understanding what you actually need and where each option is going to hurt.


Context matters more than the framework

Before comparing features, you need to define what kind of tool you’re building. A microservice that’s going to receive production traffic isn’t the same thing as an internal endpoint used by three people on the team to trigger scripts.

Internal tools typically have these characteristics:

  • Few users (technical team, operations, data)
  • Requirements that change fast (“now we also need to export to CSV”)
  • Integration with existing scripts, notebooks, or data pipelines
  • Priority on development speed over scalability
  • Short or uncertain lifecycle (might be used for three months and then discarded)

In that context, technical priorities are different from those of a production service. And that’s where FastAPI and Spring Boot truly differ.


FastAPI: when it makes sense

FastAPI shines when you need an HTTP API that’s quick to set up and integrates with the Python ecosystem. Its design is built for exactly that: define endpoints with typing, generate automatic documentation, and start with minimal configuration.

A real example. I needed an internal service that would receive a URL, launch a scraping process with httpx and BeautifulSoup, and return the structured result:

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, HttpUrl
import httpx
from bs4 import BeautifulSoup

app = FastAPI(title="Scraping Service", version="0.1.0")

class ScrapeRequest(BaseModel):
    url: HttpUrl
    selector: str = "article"

class ScrapeResult(BaseModel):
    url: str
    title: str | None
    content: str
    word_count: int

@app.post("/scrape", response_model=ScrapeResult)
async def scrape(request: ScrapeRequest):
    async with httpx.AsyncClient(timeout=30) as client:
        response = await client.get(str(request.url))
        if response.status_code != 200:
            raise HTTPException(
                status_code=502,
                detail=f"Target returned {response.status_code}"
            )

    soup = BeautifulSoup(response.text, "html.parser")
    element = soup.select_one(request.selector)

    if not element:
        raise HTTPException(status_code=404, detail="Selector not found")

    text = element.get_text(strip=True)
    return ScrapeResult(
        url=str(request.url),
        title=soup.title.string if soup.title else None,
        content=text[:5000],
        word_count=len(text.split())
    )

That’s a functional service. With uvicorn main:app --reload I have it running in seconds. The interactive docs are at /docs with zero configuration. Pydantic validates input and serializes output. If I need to add another endpoint, it’s ten more lines.

Building this in Spring Boot isn’t hard, but I need a project with structure, dependencies, Jackson configuration, a request class, a response class, a controller, possibly a service. It’s not that it’s complicated—it’s that there’s more ceremony for the same result in a context where ceremony adds nothing.


Spring Boot: when it’s still the better choice

FastAPI has its limits. And those limits show up when the project grows or when the requirements are those of a real production service.

Concrete case: an internal service that started as “an API to query configurations” and kept growing until it had authentication, database access, complex business logic, transactions, message queues, and a lifecycle spanning several years. I started that one in FastAPI, and there came a point where I missed Spring’s dependency injection, declarative transactions, Kafka integration, and the structure the framework forces you to have.

Spring Boot is the better choice when:

  • The service is going to grow and needs structure from the start
  • There’s complex business logic with transactions
  • The team already works with JVM and the service must integrate with other Java/Kotlin services
  • You need a mature ecosystem of enterprise libraries (security, messaging, batch)
  • The lifecycle is long and maintainability matters more than initial velocity

Typing: Pydantic vs Kotlin type-safe

One of the things I like most about FastAPI is Pydantic. The validation system is powerful, expressive, and integrates naturally with the framework:

from pydantic import BaseModel, Field, field_validator
from datetime import datetime
from enum import Enum

class Priority(str, Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"

class TaskCreate(BaseModel):
    name: str = Field(min_length=1, max_length=200)
    description: str | None = None
    priority: Priority = Priority.MEDIUM
    due_date: datetime | None = None

    @field_validator("due_date")
    @classmethod
    def due_date_must_be_future(cls, v):
        if v and v < datetime.now():
            raise ValueError("due_date must be in the future")
        return v

In Kotlin with Spring Boot, the equivalent would be something like:

data class TaskCreate(
    @field:NotBlank
    @field:Size(max = 200)
    val name: String,

    val description: String? = null,

    val priority: Priority = Priority.MEDIUM,

    @field:Future
    val dueDate: LocalDateTime? = null
)

enum class Priority { LOW, MEDIUM, HIGH }

Both approaches are valid. The difference lies in the philosophy:

AspectPydantic (FastAPI)Bean Validation (Spring)
ValidationIn the model, with custom validatorsAnnotations + validator classes
SerializationBuilt into the modelJackson, separate
Type coercionAutomatic (str “3” to int 3)Strict by default
DocumentationAuto-generates JSON SchemaRequires SpringDoc/Swagger
Runtime vs compileRuntime (Python)Mix: compile (Kotlin) + runtime (annotations)

Pydantic is more flexible for rapid prototyping. Kotlin’s type system is safer long-term. It’s not that one is better than the other; they serve different contexts.


Maintainability: where the difference shows over time

This is where I have to be honest about Python. A 500-line FastAPI project is a pleasure. A 15,000-line FastAPI project starts needing a lot of discipline that the language doesn’t force you to have.

Python with type hints has improved enormously, but types are optional. You can write an entire service without type hints and it runs just the same. In a two-person team maintaining an internal tool, that’s not a problem. In an eight-person team maintaining a production service for three years, optional type hints mean some of the code will have them and some won’t, and the linter warns you but doesn’t force you.

In Kotlin, the compiler forces you. You can’t pass a null where a String is expected. You can’t ignore a return type without an explicit cast. That rigidity costs you at first but pays dividends as the project grows.

My personal rule:

If the project will last fewer than six months and is maintained by the same team that built it, FastAPI. If it’s going to last longer or the team will rotate, Spring Boot with Kotlin.

It’s not an absolute rule, but it’s worked as a heuristic for me.


Integration with scripts and data tools

This is one of FastAPI’s strong points. If your organization has Python scripts for data processing, Jupyter notebooks, pipelines with pandas, or ML models, a FastAPI service integrates naturally:

from fastapi import FastAPI, BackgroundTasks
from app.pipelines import run_daily_report
from app.models import ReportConfig

app = FastAPI()

@app.post("/reports/generate")
async def generate_report(
    config: ReportConfig,
    background_tasks: BackgroundTasks
):
    background_tasks.add_task(run_daily_report, config)
    return {"status": "queued", "report_type": config.report_type}

That run_daily_report can be a function that already existed, one that uses pandas, calls external APIs with requests, generates a CSV, and uploads it to S3. You don’t need to adapt it—you just import it.

In Spring Boot, integrating with Python code requires external process calls, intermediary REST APIs, or something like GraalPython. It’s feasible but not natural.

This isn’t an argument for always using FastAPI. It’s an argument for using it when the surrounding ecosystem is Python. If the ecosystem is JVM, Spring Boot is the natural choice for the same reason.


Deployment: the practical differences

Deploying a FastAPI service and a Spring Boot service has real differences that affect daily operations.

FastAPI with Docker:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Resulting image: ~150MB. Startup time: 1-2 seconds. Memory at idle: ~50MB.

Spring Boot with Docker:

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY build/libs/app.jar .
CMD ["java", "-jar", "app.jar"]

Resulting image: ~200-300MB. Startup time: 5-15 seconds (without GraalVM). Memory at idle: ~150-300MB.

For internal tools that spin up and down frequently, or that run on serverless (Lambda, Cloud Run), the startup and memory difference is relevant. FastAPI is lighter. With GraalVM native image you can compile Spring Boot to a native binary that starts in milliseconds, but native compilation has its own problems (build time, library compatibility, reflection).

CriterionFastAPISpring BootSpring Boot + GraalVM
Docker image~150MB~250MB~100MB
Startup1-2s5-15sunder 1s
Memory at idle~50MB~200MB~50MB
Build timeSeconds30s-2min5-15min
ServerlessNaturalPossibleGood
Library ecosystempip installMaven/GradleLimited (reflection)

Comparison table by scenario

After using both in different contexts, this is my decision guide:

ScenarioRecommendationWhy
Internal API to trigger Python scriptsFastAPIDirect integration, no friction
Microservice with complex business logicSpring BootStructure, transactions, DI
ML model wrapperFastAPIPython ecosystem, async performance
Service integrating with Kafka/RabbitMQSpring BootSpring Cloud Stream, maturity
Data tool for analytics teamFastAPIPandas, notebooks, ecosystem
Service with enterprise auth (LDAP, OAuth)Spring BootSpring Security, maturity
Quick prototype to validate an ideaFastAPIDevelopment speed
Service that will last 3+ yearsSpring BootMaintainability, typing, structure
Simple CRUD with few endpointsFastAPILess ceremony
Complex workflow orchestrationDepends on the teamPython if Python infra already exists, JVM if JVM infra already exists

What I’ve learned from using them in parallel

I use both frameworks in my day-to-day. Spring Boot with Kotlin for the main services and FastAPI for auxiliary tools, prototypes, and anything that touches the data ecosystem. This coexistence has taught me a couple of things:

The first is that the choice of framework matters less than people argue on Twitter. What matters is that the tool fits the context: the team, the ecosystem, the project lifecycle, and the actual requirements (not the imagined ones).

The second is that switching frameworks has a real cost. Not just technical, but cognitive. Every time you jump from Kotlin to Python, you change mental models: mutability, typing, error handling, testing. If your team makes that jump constantly, you lose the fluency you gain from specialization.

The third is that the best internal tool is the one that gets built fast, gets used for real, and can be thrown away without drama when it stops being useful. If choosing Spring Boot for a tool that’s going to live three months means it takes you an extra week to have it ready, you’ve optimized for the wrong criterion.

Don’t choose a framework based on personal preference or the trend of the moment. Choose based on the specific context of the problem you’re going to solve. And if you’re not sure, start with the simplest thing that can work. You can always migrate later, but you rarely need to.

OshyTech

Backend and data engineering focused on scalable systems, automation, and AI.

Navigation

Copyright 2026 OshyTech. All Rights Reserved