Module Structure for Spring Boot and .NET Teams
This guide explains PolePosition's module structure for readers who may not know PolePosition or FastAPI yet.
The short version:
PolePosition gives FastAPI projects a familiar enterprise shape without turning FastAPI into Spring Boot or ASP.NET Core. A module is a small feature boundary: routes, schemas, service logic, repository code, and optional database model live together.
Mental Model
If you come from Spring Boot, think of a PolePosition module as a feature package that contains a controller, service, repository, DTOs, and entity.
If you come from ASP.NET Core, think of a PolePosition module as a feature folder with endpoints or a controller, request and response models, service logic, repository code, and an EF Core-style entity.
PolePosition keeps the same idea but uses FastAPI and SQLAlchemy names.
| Concept | Spring Boot | ASP.NET Core | PolePosition / FastAPI |
|---|---|---|---|
| Web endpoint group | @RestController |
Controller or Minimal API group | router.py with APIRouter |
| Endpoint declaration | @GetMapping, @PostMapping |
[HttpGet], MapGet, MapPost |
@router.get, @router.post |
| Request and response types | DTOs or records | DTOs, records, request models | schemas.py with Pydantic models |
| Business logic | @Service |
service class | service.py |
| Persistence boundary | @Repository |
repository or DbContext wrapper | repository.py |
| Database model | JPA @Entity |
EF Core entity | model.py with SQLAlchemy model |
| Database migrations | Flyway or Liquibase | EF Core migrations | Alembic migrations |
| App route composition | component scan plus MVC config | Program.cs route mapping |
api/router.py includes module routers |
Java and FastAPI Vocabulary
This table is intentionally more detailed for Spring Boot and Java readers.
| Java / Spring Boot | Python / FastAPI / PolePosition |
|---|---|
@Entity |
SQLAlchemy model class in model.py |
@Table(name = "...") |
__tablename__ = "..." in a SQLAlchemy model |
JPA field annotations such as @Column |
SQLAlchemy mapped_column(...) |
| DTO, record, request object | Pydantic model in schemas.py |
@Valid |
FastAPI automatically validates Pydantic request models |
Bean Validation such as @NotBlank, @Size, @Email |
Pydantic field types and Field(...) constraints |
| Validation errors | FastAPI 422 validation responses |
Lombok @Data |
Pydantic models and normal Python classes remove most boilerplate |
Lombok @Builder |
Pydantic model construction, .model_validate(...), and .model_copy(...) |
@RestController |
router.py with APIRouter |
@GetMapping, @PostMapping |
@router.get, @router.post |
@Service |
service.py service class |
@Repository |
repository.py repository class |
@Transactional |
explicit SQLAlchemy session, commit, rollback, and transaction handling |
application.yml or application.properties |
.env plus settings.py |
| Spring profiles | APP_ENV and settings-driven environment behavior |
| Flyway or Liquibase migration | Alembic revision under migrations/versions/ |
For example, Java Bean Validation:
public record CustomerCreate(
@NotBlank
@Size(max = 120)
String name
) {}
maps naturally to a Pydantic schema:
from pydantic import BaseModel, Field
class CustomerCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
When this schema is used as a FastAPI endpoint parameter, FastAPI validates the request body before your service logic runs.
.NET and FastAPI Vocabulary
This table is intentionally more detailed for ASP.NET Core and EF Core readers.
| ASP.NET Core / .NET | Python / FastAPI / PolePosition |
|---|---|
| Controller class | router.py with APIRouter |
| Minimal API route group | module router.py included with a prefix |
[HttpGet], [HttpPost], [HttpPatch] |
@router.get, @router.post, @router.patch |
Route attributes such as [Route("api/customers")] |
include_router(..., prefix="/customers") |
| Request DTO or command record | Pydantic request model in schemas.py |
| Response DTO or view model | Pydantic response model in schemas.py |
Data annotations such as [Required], [StringLength], [EmailAddress] |
Pydantic field types and Field(...) constraints |
| Model binding | FastAPI parameter and request body parsing |
| ModelState validation | FastAPI automatic validation responses |
IServiceCollection registration |
direct imports, dependency functions, and explicit wiring |
| Service class | service.py service class |
| Repository class | repository.py repository class |
| EF Core entity | SQLAlchemy model class in model.py |
DbSet<Customer> |
SQLAlchemy model plus repository queries |
DbContext |
SQLAlchemy Session and session factory |
| EF Core migrations | Alembic revisions under migrations/versions/ |
appsettings.json |
.env plus settings.py |
| ASP.NET Core environments | APP_ENV and settings-driven environment behavior |
| Middleware pipeline | FastAPI middleware in bootstrap/middleware.py |
| Exception filters or problem details middleware | exception handlers in bootstrap/errors.py |
For example, a .NET request record with data annotations:
public sealed record CustomerCreate(
[Required]
[StringLength(120, MinimumLength = 1)]
string Name
);
maps naturally to a Pydantic schema:
from pydantic import BaseModel, Field
class CustomerCreate(BaseModel):
name: str = Field(min_length=1, max_length=120)
ASP.NET Core model binding and validation usually happen before the controller action runs. FastAPI behaves similarly for Pydantic request models: invalid request bodies receive validation responses before your service logic runs.
Generated Project Shape
A generated PolePosition app uses this shape:
src/<package>/
app.py
run.py
api/
router.py
db/
base.py
models.py
session.py
modules/
status/
profile/
races/
The important folder is modules/. Each domain feature belongs there.
Standard Module Shape
When you run:
polepos add module customers
PolePosition creates:
src/<package>/modules/customers/
__init__.py
model.py
repository.py
router.py
schemas.py
service.py
tests/integration/test_customers.py
tests/unit/test_customers_service.py
It also updates:
src/<package>/api/router.py
src/<package>/db/models.py
src/<package>/modules/__init__.py
That means the module is generated, registered with the API router, and wired for Alembic model discovery.
File Responsibilities
router.py
This is closest to a Spring @RestController or an ASP.NET Core controller.
It owns HTTP route declarations.
from fastapi import APIRouter
router = APIRouter()
@router.get("/")
def list_customers():
...
In FastAPI, route decorators attach endpoints to an APIRouter. They do not
hide the route; they define it directly in normal Python code.
schemas.py
This is closest to DTOs, request models, response models, or records. PolePosition uses Pydantic models here.
from pydantic import BaseModel
class CustomerCreate(BaseModel):
name: str
class CustomerRead(BaseModel):
id: int
name: str
service.py
This is the business workflow boundary. Keep domain decisions here instead of putting all logic directly in the router.
class CustomerService:
def create_customer(self, payload: CustomerCreate):
...
repository.py
This is the persistence boundary. It uses SQLAlchemy sessions and queries.
class CustomerRepository:
def list(self):
...
model.py
This is the SQLAlchemy database model. It is closest to a JPA entity or EF Core entity.
class Customer(Base):
__tablename__ = "customers"
Schema changes should flow through Alembic migrations, not application startup.
How Router Wiring Works
polepos add module customers creates the module router and registers it in
src/<package>/api/router.py:
from <package>.modules.customers.router import router as customers_router
api_router.include_router(customers_router, prefix="/customers", tags=["customers"])
This registration happens once per module.
After that, if you add more endpoints inside:
src/<package>/modules/customers/router.py
you do not need to edit the main router again.
For example:
@router.get("/{customer_id}")
def get_customer(customer_id: int):
...
@router.patch("/{customer_id}")
def update_customer(customer_id: int):
...
Those endpoints are automatically part of the already-included customers router.
You only need another main router registration when you create another
APIRouter, another module, or a separate router file manually.
How This Differs From Spring Component Scanning
Spring Boot often discovers controllers and services through annotations and component scanning.
PolePosition does not rely on hidden component scanning. It keeps the FastAPI composition explicit:
- module endpoints use
@router.get,@router.post, and similar decorators - the module router is included once in
api/router.py - database models are imported through
db/models.pyfor Alembic metadata
This makes the project easier for humans and coding agents to inspect. The route tree is normal Python code, not hidden framework state.
How This Differs From ASP.NET Core Program.cs
ASP.NET Core often maps controllers or endpoint groups in Program.cs.
PolePosition uses api/router.py for that composition role. It is the central
API router file:
src/<package>/api/router.py
The FastAPI app includes that API router in app.py, and each module router is
included under it.
API-Only Modules
If a feature does not need a database model or repository, use:
polepos add module webhooks --api-only
This creates:
src/<package>/modules/webhooks/
__init__.py
router.py
schemas.py
service.py
Use this for callbacks, health-adjacent endpoints, transformation endpoints, or thin orchestration surfaces that do not need database tables yet.
AI Prompt Modules
If a feature is an LLM prompt workflow, use:
polepos add module assistant --template ai-prompt
This creates a module with:
orchestrator.py
prompts.py
router.py
schemas.py
service.py
It also creates shared integrations/llm adapter stubs when missing.
What To Edit After Generation
For a real domain, expect to edit:
model.py: database fields and table shapeschemas.py: request and response contractsservice.py: business rulesrepository.py: queries and persistence behaviorrouter.py: endpoint paths and HTTP behavior- generated tests: examples of expected behavior
The generated module is a strong starting point, not the final business system.
What Not To Do
Avoid these patterns:
- do not put all features in one global
services/folder - do not create tables during FastAPI startup
- do not bypass Alembic for schema changes
- do not manually recreate module boilerplate when
polepos add modulefits - do not remove PolePosition-managed markers unless you intentionally opt out
Lifecycle Flow
Use this flow when growing a REST API:
polepos add module customers
# edit model.py, schemas.py, service.py, repository.py, router.py
polepos check
polepos db revision -m "add customers table"
polepos db upgrade
uv run pytest
For coding agents and LLMs, the rule is:
When the user asks for a new domain feature, prefer generating a PolePosition module first, then reshape that module for the real domain.