slowlp
← Blog
Method 2026.06.22 · 8 min read

Common Modules and Base Classes to End Code Duplication

Around the third domain, lift the repeating code up into common

Method

So I Stop Writing the Same Code Twice: Common Modules, Base Classes, and Naming Rules

Around the third domain, lift the repeating code up into common

Once you’ve built a couple of domains, a strange sense of déjà vu sets in. That code you just wrote — pretty sure you wrote almost exactly the same thing in the domain next door. Every model gets an id plus created and updated timestamp fields. Every repository repeats get-by-id, list, create, delete. Every schema carries the same config. You can get by with copy-paste for a while. Then around the third domain it hits you: this should have lived in one place.

So I carve out a spot called common. It’s where the foundational code goes — the stuff that isn’t tied to any one piece of business logic but gets shared across every domain. I split it by role.

  • base/ — base classes for models, schemas, and repositories
  • config/ — environment variables, logging setup
  • exception/ — shared exceptions
  • schema/ — common data structures that don’t belong to any single domain
  • util/ — standalone utilities like query builders and date/time helpers

Base Classes: Pull the Repeating Parts Upward

The heart of it is base/. Define a single BaseSqlModel that every table model inherits from, and every model gets the common fields — id, inserted_at, updated_at — plus the create and update methods for free. A new domain model only adds its own fields. Same story with repositories. Write get-by-id, get-by-condition, list, pagination, create, and delete once in BaseRepository, and each domain repository inherits that and only adds the queries specific to its own domain. Schemas do the same through BaseSchema, BaseCreateSchema, and BaseUpdateSchema, lifting the shared config and model-conversion logic up.

Laid out as an inheritance hierarchy, it looks like this.

# common/base: written once
class BaseSqlModel(SQLModel):
    id: int | None = Field(default=None, primary_key=True)
    # inserted_at, updated_at, create/update methods ...

# each domain: adds only its own
class UserAccount(BaseSqlModel, table=True):
    name: str
    email: str

The rule is simple. If two domains repeat the same thing, lift it into base. If it only lives in one domain, leave it where it is.

Keep Configuration Out of the Code

For the same reason, configuration gets gathered in one place too. But how you gather it matters: don’t hardcode the values, read them from environment variables. Things like the DB address and external service URLs differ across dev, test, and production. Hardcode them and you have to edit code every time the environment changes.

So I keep a single settings class and declare the environment variables along with their types.

from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    DATABASE_URL: str
    APP_ENV: str = "dev"

    model_config = SettingsConfigDict(env_file=".env", extra="ignore")

settings = Settings()

When the app boots, it reads the environment variables and builds the settings object, and the rest of the code just imports that. There’s a nice bonus: if a required variable is missing, the app doesn’t start at all — it errors out immediately. It catches a missing setting the moment you start up, not somewhere deep into runtime.

I stop here. How you inject these through Docker, or how a Celery worker picks them up, varies by environment. Just take the principle with you: values live outside the code, in the environment.

Consistent Names Make Code Readable

The last piece is naming. It sounds trivial, but it’s the part you bump into most every single day. The baseline follows PEP 8, with a rule fixed for each spot.

  • modules (files) — snake_case (account_router.py)
  • classes — PascalCase (UserAccountRepository, NotFoundException)
  • functions, methods, variables — snake_case (create_account, account_id)
  • constants — ALL_CAPS (MAX_RETRIES)

What matters isn’t the rule itself but the consistency. When you can read “this is a class, that’s a constant, this is a function” straight from the name, your brain spends one less beat parsing the code. Grouping imports in standard → third-party → internal order and writing them as absolute paths is the same idea. Open a file and you can see at a glance where each piece of code came from.

Type hints belong here too. Annotate arguments, returns, and variables, and your IDE autocomplete comes alive and you catch type errors while you’re still writing. FastAPI, Pydantic, and SQLModel lean hard on type hints, so in this stack they’re all but mandatory.

Kill duplication with a common module, push configuration out of the code, keep names consistent. All three are favors to your future self. The moment your domain count climbs past three, you start to feel it.

From the project
Stock Portfolio App (MyFolio) LAB
A portfolio-first investing app — no need to analyze individual stocks, just connect your KIS brokerage account and let the app handle order planning. Running privately on my home server.
View →
COMMENTS