{"id":2391,"date":"2026-06-01T00:12:00","date_gmt":"2026-06-01T10:12:00","guid":{"rendered":"https:\/\/mshaeri.com\/blog\/?p=2391"},"modified":"2026-07-28T11:44:19","modified_gmt":"2026-07-28T21:44:19","slug":"fastapi-sqlalchemy-best-practices","status":"publish","type":"post","link":"https:\/\/mshaeri.com\/blog\/fastapi-sqlalchemy-best-practices\/","title":{"rendered":"FastAPI + SQLAlchemy Best Practices"},"content":{"rendered":"\n<p>Most long-term problems, both in development and in production come from rushed model design, scattered logics, and patterns that feel convenient at first but become expensive later.<\/p>\n\n\n\n<p>As Robert C. Martin writes in <strong>Clean Architecture<\/strong>, <em><strong>&#8220;getting something to work\u2014once\u2014just isn\u2019t that hard<\/strong><\/em>, <strong>g<\/strong><em><strong>etting it right is another matter entirely.&#8221;<\/strong><\/em> That is exactly the point here. Making an API work is not the hard part. Keeping a codebase readable and easy to maintain as it grows is the harder part. In this post I go through a set of practical best practices for <strong>FastAPI + SQLAlchemy <\/strong>applications that I have learnt from projects. They are patterns that helps keeping codebases maintainable and pleasant to work with over time.<\/p>\n\n\n\n<p>This post covers best practices in these topics:<\/p>\n\n\n\n<ol>\n<li><a href=\"#fastapi-Number-Of-Models-Per-Bounded-Context\" data-type=\"internal\" data-id=\"#fastapi-Number-Of-Models-Per-Bounded-Context\">Optimal number of models per bounded context<\/a><\/li>\n\n\n\n<li><a href=\"#fastapi-sqlalchemy-Encapsulate-Reusable-Queries\" data-type=\"internal\" data-id=\"#fastapi-sqlalchemy-Encapsulate-Reusable-Queries\">Keep reusable query logic close to the model<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-Avoid-Two-Different-Meanings-Of-No-String-Value\">Empty values in string columns<\/a><\/li>\n\n\n\n<li><a href=\"#Keep-Alembic-Migrations-Clean,-But-Treat-Applied-Revisions-As-Immutable\">Keep Alembic migrations clean<\/a><\/li>\n\n\n\n<li><a href=\"#Use-Two-Identifiers:-Private-PK-And-Public-UUID\">Public UUIDs for API-facing identifiers<\/a><\/li>\n\n\n\n<li><a href=\"#Use-SQL-Expressions-Instead-Of-Processing-Rows-In-Python\">SQLAlchemy expression tools instead of Python loops<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-Add-A-Generic-pk-Property-On-Your-Base-Model\">Generic <code>pk<\/code> access on the base model<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-Name-Models-And-Relationships-Clearly\">Model and relationship naming<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-fastapi-Use-A-yield-Dependency-With-async_sessionmaker\">Use a <code>yield<\/code> dependency with <code>async_sessionmaker<\/code><\/a><\/li>\n\n\n\n<li><a href=\"#fastapi-sqlalchemy-Keep-Transaction-Boundaries-Explicit\">Keep transaction boundaries explicit<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-fastapi-Avoid-Accidental-Lazy-Loading-And-N+1-Queries\">Avoid accidental lazy loading and N+1 queries<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-Do-Not-Commit-Inside-Low-Level-Helpers\" data-type=\"internal\" data-id=\"#sqlalchemy-Do-Not-Commit-Inside-Low-Level-Helpers\">Do not commit inside low-level helpers<\/a><\/li>\n\n\n\n<li><a href=\"#Avoid-COUNT-When-You-Only-Need-To-Know-Whether-Something-Exists\">Avoid <code>COUNT(*)<\/code> when it is unnecessary<\/a><\/li>\n\n\n\n<li><a href=\"#fastapi-sqlalchemy-Keep-Business-Logic-Close-To-The-Model\">Keep business logic in one place<\/a><\/li>\n\n\n\n<li><a href=\"#sqlalchemy-Use-with_for_update-Wisely\">Use <code>with_for_update()<\/code> wisely<\/a><\/li>\n\n\n\n<li><a href=\"#Enforce-Data-Consistency-at-the-Database-Level\">Enforce data consistency at the database level<\/a><\/li>\n\n\n\n<li><a href=\"#Make-Full-Use-of-Dependency-Injection-to-Keep-Routes-Clean-and-Thin\" data-type=\"internal\" data-id=\"#Make-Full-Use-of-Dependency-Injection-to-Keep-Routes-Clean-and-Thin\">Make Full Use of Dependency Injection to Keep Routes Clean and Thin<\/a><\/li>\n\n\n\n<li><a href=\"#Set-Explicit-Naming-Conventions-for-Database-Constraints\" data-type=\"internal\" data-id=\"#Set-Explicit-Naming-Conventions-for-Database-Constraints\">Set Explicit Naming Conventions for Database Constraints<\/a><\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fastapi-Number-Of-Models-Per-Bounded-Context\">1. Keep The Number Of Models Per Bounded Context Reasonable<\/h2>\n\n\n\n<p>If you have twenty unrelated models in a single <code>models.py<\/code>, your codebase is trying to tell you something.<\/p>\n\n\n<div class=\"wp-block-image is-style-default\">\n<figure class=\"aligncenter size-large is-resized\"><a href=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"611\" src=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-1024x611.png\" alt=\"Keep The Number Of Models Per Bounded Context Reasonable in fastapi\" class=\"wp-image-2453\" style=\"width:732px;height:auto\" title=\"Keep The Number Of Models Per Bounded Context Reasonable in fastapi\" srcset=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-1024x611.png 1024w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-300x179.png 300w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-768x459.png 768w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-890x530.png 890w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4-445x265.png 445w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-4.png 1313w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Break down the app to keep models number no more than 10<\/figcaption><\/figure><\/div>\n\n\n<p>In FastAPI applications, it is common to start with a flat structure and then let it grow without boundaries. That works for very small apps, but it becomes messy fast. Prefer splitting your codebase by domain or bounded context:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">app\/\n  billing\/\n    models.py\n    schemas.py\n    service.py\n    routes.py\n  catalog\/\n    models.py\n    schemas.py\n    service.py\n    routes.py<\/code><\/pre>\n\n\n\n<p>There is no magical hard limit, but once a module holds around ten models, it is worth asking whether more than one business domain has been mixed together. See this link for <a href=\"https:\/\/github.com\/zhanymkanov\/fastapi-best-practices#project-structure\">preferred fastapi project structure<\/a><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fastapi-sqlalchemy-Encapsulate-Reusable-Queries\">2. Encapsulate Reusable Queries<\/h2>\n\n\n\n<p>In Django, custom managers are a natural place for repeated query logic. In <strong>SQLAlchemy <\/strong>the equivalent idea is to centralize reusable statements instead of scattering ad-hoc <code>select()<\/code> blocks across route handlers.<\/p>\n\n\n\n<p>A nice pattern is to keep small query helpers close to the model as <code>@classmethod<\/code>s that return a <code>Select<\/code> object. That gives you manager-like ergonomics without making the model responsible for session lifecycle, transactions, or endpoint behavior.<\/p>\n\n\n\n<p>For example, avoid this repeated pattern:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.get(\"\/posts\/published\")\nasync def get_published_posts(session: SessionDep):\n    stmt = select(Post).where(Post.status == \"published\")\n    return (await session.scalars(stmt)).all()\n\n\n@app.get(\"\/authors\/{author_id}\/posts\")\nasync def get_author_posts(author_id: int, session: SessionDep):\n    stmt = (\n        select(Post)\n        .where(Post.status == \"published\", Post.author_id == author_id)\n        .order_by(Post.created_at.desc())\n    )\n    return (await session.scalars(stmt)).all()<\/code><\/pre>\n\n\n\n<p>Reusable statement builders, and putting them on the model is often a nice option:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Post(Base):\n    __tablename__ = \"posts\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    author_id: Mapped[int] = mapped_column(ForeignKey(\"authors.id\"))\n    status: Mapped[str] = mapped_column(String(20))\n    title: Mapped[str] = mapped_column(String(200))\n    created_at: Mapped[datetime] = mapped_column(\n        server_default=func.now(), nullable=False\n    )\n\n    @classmethod\n    def published_stmt(cls):\n        return select(cls).where(cls.status == \"published\")\n\n    @classmethod\n    def published_by_author_stmt(cls, author_id: int):\n        return (\n            cls.published_stmt()\n            .where(cls.author_id == author_id)\n            .order_by(cls.created_at.desc(), cls.id.desc())\n        )<\/code><\/pre>\n\n\n\n<p>Then let a service use those model helpers:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def get_published_posts_by_author(\n    session: AsyncSession,\n    author_id: int,\n) -&gt; list[Post]:\n    stmt = Post.published_by_author_stmt(author_id)\n    return (await session.scalars(stmt)).all()<\/code><\/pre>\n\n\n\n<p>And keep the route thin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.get(\"\/authors\/{author_id}\/posts\")\nasync def get_author_posts(author_id: int, session: SessionDep):\n    return await get_published_posts_by_author(session, author_id)<\/code><\/pre>\n\n\n\n<p>This keeps separation of concerns clean. The route calls a service, and the service composes model-level query helpers. It gives you several benefits:<\/p>\n\n\n\n<ul>\n<li>query intent stays close to the model it belongs to<\/li>\n\n\n\n<li>routes stay focused on HTTP concerns<\/li>\n\n\n\n<li>services coordinate application use cases<\/li>\n\n\n\n<li>the returned object is still just a SQLAlchemy statement, so it composes well<\/li>\n\n\n\n<li>query behavior is easier to test and harder to accidentally fork into slightly different versions<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large is-resized\"><a href=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"394\" src=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2-1024x394.png\" alt=\"separation of concerns in fastapi sqlalchemy by encapsulating reusable queries in the model\" class=\"wp-image-2399\" style=\"width:960px;height:auto\" title=\"separation of concerns in fastapi sqlalchemy by encapsulating reusable queries in the model\" srcset=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2-1024x394.png 1024w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2-300x115.png 300w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2-768x295.png 768w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2-1536x590.png 1536w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-2.png 1746w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">separation of concerns in fastapi + sqlalchemy by encapsulating reusable queries in the model and keep services and routes thin<\/figcaption><\/figure><\/div>\n\n\n<p>What we should avoid is putting session-bound methods directly on the model, such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@classmethod\nasync def get_published(cls, session: AsyncSession):\n    return (await session.scalars(select(cls).where(cls.status == \"published\"))).all()<\/code><\/pre>\n\n\n\n<p>That starts mixing model definition with I\/O and transaction concerns. If a model method executes the session internally and returns a raw list of objects, you lose composability, it means you can not apply more filter on <code>get_published()<\/code>. Returning a statement is usually the cleaner boundary, and it keeps the code easier to test and maintain.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-Avoid-Two-Different-Meanings-Of-No-String-Value\">3. Avoid Two Different Meanings Of &#8220;No String Value&#8221;<\/h2>\n\n\n\n<p>One of the most common data quality problems in APIs is allowing both <code>NULL<\/code> and <code>\"\"<\/code> to mean &#8220;missing text&#8221;. Pick one representation and normalize input consistently.<\/p>\n\n\n\n<p>In <strong>SQLAlchemy<\/strong>, using <code>None<\/code> \/ <code>NULL<\/code> is usually the cleanest choice for optional string data:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class CustomerCreate(BaseModel):\n    middle_name: str | None = None\n\n    @field_validator(\"middle_name\")\n    @classmethod\n    def normalize_middle_name(cls, value: str | None) -&gt; str | None:\n        if value is None:\n            return None\n        value = value.strip()\n        return value or None\n\n\nclass Customer(Base):\n    __tablename__ = \"customers\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    middle_name: Mapped[str | None] = mapped_column(String(100), nullable=True)<\/code><\/pre>\n\n\n\n<p>For fields where empty string is a meaningful value, keep <code>nullable=False<\/code> and store <code>\"\"<\/code> consistently. The important part is not which convention you choose. The important part is avoiding both at the same time.<\/p>\n\n\n\n<p>For optional unique string fields, <code>NULL<\/code> is usually the better choice than <code>\"\"<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Keep-Alembic-Migrations-Clean,-But-Treat-Applied-Revisions-As-Immutable\">4. Keep Alembic Migrations Clean<\/h2>\n\n\n\n<p><strong>FastAPI + SQLAlchemy<\/strong> projects usually rely on Alembic for schema changes. A common mistake is letting migration history become noisy and chaotic during active feature development.<\/p>\n\n\n\n<p>For example, one feature branch might generate these revisions:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">001_add_order_table.py\n002_fix_order_table.py\n003_fix_order_table_again.py\n004_add_missing_index.py<\/code><\/pre>\n\n\n\n<p>Before merging a private branch, squash all migration generated in the brnach into one clean revision. However, there exist cases where the migration should be applied in more than one phase, in that cases having more than one migration file is inevitable. <\/p>\n\n\n\n<p>Typical flow:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"bash\" class=\"language-bash\">alembic revision --autogenerate -m \"add orders table\"\nalembic upgrade head<\/code><\/pre>\n\n\n\n<p>Best practice:<\/p>\n\n\n\n<ul>\n<li>keep branch-local migrations tidy<\/li>\n\n\n\n<li>write descriptive revision names<\/li>\n\n\n\n<li>do not rewrite migrations already used by teammates or production<\/li>\n\n\n\n<li>prefer one clean migration per coherent change instead of five correction migrations in a row<\/li>\n<\/ul>\n\n\n\n<p>Alembic does not have the same built-in &#8220;squash migrations&#8221; workflow Django has, so cleanup usually means reorganizing revisions before they become shared history.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Use-Two-Identifiers:-Private-PK-And-Public-UUID\">5. Use Two Identifiers: Private PK And Public UUID<\/h2>\n\n\n\n<p>In API-driven applications, it is often useful to keep a compact internal primary key and expose a public UUID externally.<\/p>\n\n\n\n<p>Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Order(Base):\n    __tablename__ = \"orders\"\n\n    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)\n    public_id: Mapped[uuid.UUID] = mapped_column(\n        Uuid,\n        default=uuid.uuid4,\n        unique=True,\n        nullable=False,\n        index=True,\n    )\n    status: Mapped[str] = mapped_column(String(30), nullable=False)<\/code><\/pre>\n\n\n\n<p>Then your public routes can use <code>public_id<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.get(\"\/orders\/{public_id}\")\nasync def get_order(public_id: uuid.UUID, session: SessionDep):\n    stmt = select(Order).where(Order.public_id == public_id)\n    return await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p>This pattern gives us:<\/p>\n\n\n\n<ul>\n<li>small and efficient internal joins on integer keys<\/li>\n\n\n\n<li>stable public identifiers<\/li>\n\n\n\n<li>less leakage of record counts and ordering<\/li>\n<\/ul>\n\n\n\n<p>Also note the use of SQLAlchemy&#8217;s backend-agnostic <code>Uuid<\/code> type. In SQLAlchemy 2.x, you no longer need custom GUID hacks for common cross-database UUID storage.<\/p>\n\n\n\n<p>Although there are valid distributed-system cases for UUID or ULID primary keys, for many business apps, integer primary key plus public UUID is a very pragmatic default.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Use-SQL-Expressions-Instead-Of-Processing-Rows-In-Python\">6. Use SQL Expressions Instead Of Processing Rows In Python<\/h2>\n\n\n\n<p>If the database can do the filtering, comparison, aggregation, or update, let it do it.<\/p>\n\n\n\n<p>Bad practice:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">students = []\nresult = await session.scalars(select(Student))\nfor student in result:\n    if student.math_score &gt; student.english_score:\n        students.append(student)<\/code><\/pre>\n\n\n\n<p>Better:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">stmt = select(Student).where(Student.math_score &gt; Student.english_score)\nstudents = (await session.scalars(stmt)).all()<\/code><\/pre>\n\n\n\n<p><strong>SQLAlchemy <\/strong>gives you rich expression tools such as:<\/p>\n\n\n\n<ul>\n<li><code>func<\/code> for SQL functions<\/li>\n\n\n\n<li>subqueries and CTEs<\/li>\n\n\n\n<li>SQL expressions in updates<\/li>\n\n\n\n<li>relationship loader options<\/li>\n<\/ul>\n\n\n\n<p>Another useful example is atomic updates:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">post = await session.get(Post, post_id)\npost.view_count = Post.view_count + 1\nawait session.commit()<\/code><\/pre>\n\n\n\n<p>Letting the database express the change is safer than reading a value in Python, incrementing it, and writing it back later.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-Add-A-Generic-pk-Property-On-Your-Base-Model\">7. Add A Generic pk Property On Your Base Model<\/h2>\n\n\n\n<p>One of the nicest ideas in <strong>Django models <\/strong>is <code>obj.pk<\/code>. It lets you refer to the primary key generically without coupling your code to a specific column name like <code>id<\/code>, <code>uuid<\/code>, or <code>invoice_number<\/code>.<\/p>\n\n\n\n<p>You can get the same convenience in <strong>SQLAlchemy <\/strong>with a hybrid property on your declarative base:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import inspect\nfrom sqlalchemy.ext.hybrid import hybrid_property\nfrom sqlalchemy.orm import DeclarativeBase\n\n\nclass Base(DeclarativeBase):\n&nbsp; &nbsp; @classmethod\n&nbsp; &nbsp; def _get_pk_field(cls):\n&nbsp; &nbsp; &nbsp; &nbsp; mapper = inspect(cls)\n&nbsp; &nbsp; &nbsp; &nbsp; fields = mapper.primary_key\n\n&nbsp; &nbsp; &nbsp; &nbsp; if len(fields) != 1:\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raise ValueError(f\"{cls.__name__} has composite primary key\")\n\n&nbsp; &nbsp; &nbsp; &nbsp; return fields[0]\n\n&nbsp; &nbsp; @hybrid_property\n&nbsp; &nbsp; def pk(self):\n&nbsp; &nbsp; &nbsp; &nbsp; pk_field = type(self)._get_pk_field()\n&nbsp; &nbsp; &nbsp; &nbsp; return getattr(self, pk_field.key)\n\n&nbsp; &nbsp; @pk.expression\n&nbsp; &nbsp; def pk(cls):\n&nbsp; &nbsp; &nbsp; &nbsp; return cls._get_pk_field()<\/code><\/pre>\n\n\n\n<p>This has the same benefits Django developers enjoy:<\/p>\n\n\n\n<ul>\n<li>your code stays generic even when different models use different primary-key names<\/li>\n\n\n\n<li><code>pk<\/code> expresses intent better than hardcoding <code>id<\/code><\/li>\n\n\n\n<li>the same API works on instances and inside SQL expressions<\/li>\n\n\n\n<li>schema refactors are less painful because calling code does not care what the PK column is called<\/li>\n<\/ul>\n\n\n\n<p>Example model:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Todo(Base):\n    __tablename__ = \"todos\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    title: Mapped[str] = mapped_column(String(200))<\/code><\/pre>\n\n\n\n<p>Instance access:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">todo = Todo(title=\"Write the post\")\ntodo.pk<\/code><\/pre>\n\n\n\n<p>Class-level SQL expression:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">Todo.pk<\/code><\/pre>\n\n\n\n<p>Comparison expression:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">Todo.pk == 1<\/code><\/pre>\n\n\n\n<p>Selecting the primary key generically:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">select(Todo.pk)<\/code><\/pre>\n\n\n\n<p>Filtering by primary key without hardcoding <code>id<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">stmt = select(Todo).where(Todo.pk == 1)\ntodo = await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p>When the primary key is not named <code>id<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Invoice(Base):\n    __tablename__ = \"invoices\"\n\n    invoice_number: Mapped[str] = mapped_column(String(50), primary_key=True)\n    amount_cents: Mapped[int]<\/code><\/pre>\n\n\n\n<p>Now the calling code stays consistent:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">stmt = select(Invoice).where(Invoice.pk == \"INV-2026-0001\")\ninvoice = await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p><code><strong>session.get()<\/strong><\/code> is still a great API for direct primary-key retrieval when you already have the PK value in hand. But adding <code>Base.pk<\/code> gives your models the same expressive, generic primary-key interface that makes Django code pleasant to write and maintain. <\/p>\n\n\n\n<p>One improvement would be to define two reusable base classes: a primary base class without an <code>id<\/code> field, which still provides the <code>pk<\/code> property described above, and a second base class that inherits from it and adds <code>id<\/code> as the default primary key. This gives each model the flexibility to either use the conventional integer <code>id<\/code> primary key or define its own primary key, such as a UUID, slug, or another natural key.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import inspect\nfrom sqlalchemy.ext.hybrid import hybrid_property\nfrom sqlalchemy.orm import DeclarativeBase\n\n\nclass Base(DeclarativeBase):\n&nbsp; &nbsp; @classmethod\n&nbsp; &nbsp; def _get_pk_field(cls):\n&nbsp; &nbsp; &nbsp; &nbsp; mapper = inspect(cls)\n&nbsp; &nbsp; &nbsp; &nbsp; fields = mapper.primary_key\n\n&nbsp; &nbsp; &nbsp; &nbsp; if len(fields) != 1:\n&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; raise ValueError(f\"{cls.__name__} has composite primary key\")\n\n&nbsp; &nbsp; &nbsp; &nbsp; return fields[0]\n\n&nbsp; &nbsp; @hybrid_property\n&nbsp; &nbsp; def pk(self):\n&nbsp; &nbsp; &nbsp; &nbsp; pk_field = type(self)._get_pk_field()\n&nbsp; &nbsp; &nbsp; &nbsp; return getattr(self, pk_field.key)\n\n&nbsp; &nbsp; @pk.expression\n&nbsp; &nbsp; def pk(cls):\n&nbsp; &nbsp; &nbsp; &nbsp; return cls._get_pk_field()\n\n\nclass BaseWithId(Base):\n    __abstract__ = True\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n\n\nclass User(BaseWithId):\n    __tablename__ = \"users\"\n\n    name: Mapped[str]\n\n\nclass Country(Base):\n    __tablename__ = \"countries\"\n\n    code: Mapped[str] = mapped_column(primary_key=True)\n    name: Mapped[str]\n<\/code><\/pre>\n\n\n\n<p>In this example, <code>User<\/code> uses the default integer <code>id<\/code>, while <code>Country<\/code> uses <code>code<\/code> as its primary key. Both models can still access their primary-key value through the shared <code>pk<\/code> property.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-Name-Models-And-Relationships-Clearly\">8. Name Models And Relationships Clearly<\/h2>\n\n\n\n<p>Naming matters because ORM code is read far more often than it is written. A few practical conventions:<\/p>\n\n\n\n<ul>\n<li>class names should be singular: <strong><code>User<\/code>, <code>Order<\/code>, <code>BlogPost<\/code><\/strong><\/li>\n\n\n\n<li>table names should be predictable: <strong><code>users<\/code>, <code>orders<\/code>, <code>blog_posts<\/code><\/strong><\/li>\n\n\n\n<li>scalar relationships should be singular: <strong><code>author<\/code>, <code>profile<\/code>, <code>customer<\/code><\/strong><\/li>\n\n\n\n<li>collection relationships should be plural: <strong><code>posts<\/code>, <code>items<\/code>, <code>roles<\/code><\/strong><\/li>\n<\/ul>\n\n\n\n<p>Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Author(Base):\n    __tablename__ = \"authors\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str] = mapped_column(String(100))\n    posts: Mapped[list[\"BlogPost\"]] = relationship(back_populates=\"author\")\n\n\nclass BlogPost(Base):\n    __tablename__ = \"blog_posts\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    author_id: Mapped[int] = mapped_column(ForeignKey(\"authors.id\"))\n    title: Mapped[str] = mapped_column(String(200))\n\n    author: Mapped[Author] = relationship(back_populates=\"posts\")<\/code><\/pre>\n\n\n\n<p>This is clearer than ambiguous names like <code>post_data<\/code>, <code>author_ref<\/code>, or <code>blogpostlist<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-fastapi-Use-A-yield-Dependency-With-async_sessionmaker\">9. Use A <code>yield<\/code> Dependency With <code>async_sessionmaker<\/code><\/h2>\n\n\n\n<p>FastAPI&#8217;s documentation recommends using dependencies with <code>yield<\/code> when you need setup and teardown around a request. For database sessions, that maps perfectly to SQLAlchemy sessions: create the session before <code>yield<\/code>, and make sure it is closed after the request finishes.<\/p>\n\n\n\n<p>For modern async SQLAlchemy code, the cleanest version is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine\n\n\nengine = create_async_engine(\n    DATABASE_URL,\n    pool_size=10,\n    max_overflow=20,\n    pool_timeout=30,\n    pool_pre_ping=True,\n)\n\nSessionLocal = async_sessionmaker(engine, expire_on_commit=False)\n\n\nasync def get_session():\n    async with SessionLocal() as session:\n        yield session<\/code><\/pre>\n\n\n\n<p>It is better to use FastAPI&#8217;s <code>yield<\/code> dependency pattern for request-scoped setup and cleanup. And use SQLAlchemy&#8217;s <code>async_sessionmaker<\/code>, which is the dedicated SQLAlchemy 2.x factory for <code>AsyncSession<\/code>. It&#8217;s clearer and more modern than using generic <code>sessionmaker(..., class_=AsyncSession)<\/code><\/p>\n\n\n\n<p>Benefits:<\/p>\n\n\n\n<ul>\n<li>each request gets its own session<\/li>\n\n\n\n<li>the session is closed automatically after the request<\/li>\n\n\n\n<li>the code is small and easy to understand<\/li>\n<\/ul>\n\n\n\n<p>If you prefer manual cleanup, this is also valid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def get_session():\n    session = SessionLocal()\n    try:\n        yield session\n    finally:\n        await session.close()<\/code><\/pre>\n\n\n\n<p>But <code><mark style=\"background-color:rgba(0, 0, 0, 0)\" class=\"has-inline-color has-vivid-purple-color\"><strong>async with SessionLocal() as session:<\/strong><\/mark><\/code> is usually cleaner and less error-prone.<\/p>\n\n\n\n<p>Also, an <code>AsyncSession<\/code> should not be shared across concurrent tasks. The safe mental model is one session per request, and if you spawn concurrent tasks, each task should have its own session.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fastapi-sqlalchemy-Keep-Transaction-Boundaries-Explicit\">10. Keep Transaction Boundaries Explicit<\/h2>\n\n\n\n<p>This is one of the most important <strong>SQLAlchemy <\/strong>habits to get right in FastAPI apps:  understand when you need <code>flush()<\/code> and when you need <code>commit()<\/code>.<\/p>\n\n\n\n<ul>\n<li><code><strong>flush()<\/strong><\/code> sends pending SQL to the database inside the current transaction<\/li>\n\n\n\n<li><strong><code>commit()<\/code> <\/strong>permanently finishes the transaction<\/li>\n<\/ul>\n\n\n\n<p>Many bugs come from calling<strong> <code>commit()<\/code><\/strong> too early just to get a generated primary key or to make a later insert work.<\/p>\n\n\n\n<p>Bad:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.post(\"\/orders\")\nasync def create_order(payload: OrderCreate, session: SessionDep):\n    order = Order(customer_id=payload.customer_id, status=\"pending\")\n    session.add(order)\n    await session.commit()\n\n    item = OrderItem(order_id=order.id, sku=payload.sku, quantity=payload.quantity)\n    session.add(item)\n    await session.commit()\n    return order<\/code><\/pre>\n\n\n\n<p>Good:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.post(\"\/orders\")\nasync def create_order(payload: OrderCreate, session: SessionDep):\n    order = Order(customer_id=payload.customer_id, status=\"pending\")\n    session.add(order)\n    await session.flush()\n\n    item = OrderItem(order_id=order.id, sku=payload.sku, quantity=payload.quantity)\n    session.add(item)\n\n    await session.commit()\n    await session.refresh(order)\n    return order<\/code><\/pre>\n\n\n\n<p>It is better because:<\/p>\n\n\n\n<ul>\n<li>one request owns one transaction<\/li>\n\n\n\n<li>you still get <code><strong>order.id<\/strong><\/code> after <code><strong>flush()<\/strong><\/code><\/li>\n\n\n\n<li>if creating the item fails, the whole unit of work can roll back together<\/li>\n\n\n\n<li>transaction behavior stays predictable<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><a href=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"285\" src=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3-1024x285.png\" alt=\"Keep Transaction Boundaries Explicit in fast api sqlalchemy. use flush and commit wisely\" class=\"wp-image-2433\" title=\"Keep Transaction Boundaries Explicit in fast api sqlalchemy. use flush and commit wisely\" srcset=\"https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3-1024x285.png 1024w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3-300x84.png 300w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3-768x214.png 768w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3-1536x428.png 1536w, https:\/\/mshaeri.com\/blog\/wp-content\/uploads\/2026\/05\/image-3.png 1741w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption class=\"wp-element-caption\">Keep Transaction Boundaries Explicit in fast api sqlalchemy by using flush and commit in right places<\/figcaption><\/figure><\/div>\n\n\n<p>Use<strong> <code>flush()<\/code><\/strong> when you need database-generated values or constraint checks before the end of the request. Use<strong> <code>commit()<\/code> <\/strong>at the use-case boundary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-fastapi-Avoid-Accidental-Lazy-Loading-And-N+1-Queries\">11. Avoid Accidental Lazy Loading And N+1 Queries<\/h2>\n\n\n\n<p><strong>FastAPI <\/strong>endpoints serialize lists of ORM objects into JSON. That is exactly where accidental lazy loading can turn one query into fifty (N+1 query issue).<\/p>\n\n\n\n<p>Example problem:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.get(\"\/posts\")\nasync def get_posts(session: SessionDep):\n    posts = (await session.scalars(select(Post))).all()\n\n    return [\n        {\n            \"id\": post.id,\n            \"title\": post.title,\n            \"author_name\": post.author.name,\n        }\n        for post in posts\n    ]<\/code><\/pre>\n\n\n\n<p>If <code>author<\/code> is lazily loaded, every <code>post.author<\/code> access can trigger another query hitting DB.<\/p>\n\n\n\n<p>Prefer explicit loader strategies:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy.orm import selectinload\n\n\n@app.get(\"\/posts\")\nasync def get_posts(session: SessionDep):\n    stmt = select(Post).options(joinedload(Post.author))\n    posts = (await session.scalars(stmt)).all()\n\n    return [\n        {\n            \"id\": post.id,\n            \"title\": post.title,\n            \"author_name\": post.author.name,\n        }\n        for post in posts\n    ]<\/code><\/pre>\n\n\n\n<p><code><strong>joinedload<\/strong>()<\/code> fetches the parent and related rows in one SQL query using a <code>JOIN<\/code>, while <code><strong>selectinload<\/strong>()<\/code> fetches the parent rows first and then loads related rows in a second query with <code><strong>IN (...)<\/strong><\/code>.<br>A practical rule of thumb is this: prefer <code><strong>selectinload<\/strong>()<\/code> for collections like <strong><code>posts.comments<\/code>, <code>users.roles<\/code>, <\/strong>or <strong><code>orders.items<\/code><\/strong>; consider <code><strong>joinedload<\/strong>()<\/code> for scalar relationships like <strong><code>post.author<\/code> or <code>order.customer<\/code><\/strong>, especially when the result set is small.<br>If a <strong><code>JOIN<\/code> <\/strong>would duplicate parent rows heavily, <code><strong>selectinload<\/strong>()<\/code> is usually the better choice; if you need everything in one compact query and the relationship is small, <code><strong>joinedload<\/strong>()<\/code> can be a very good fit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-Do-Not-Commit-Inside-Low-Level-Helpers\">12. Do Not Commit Inside Low-Level Helpers<\/h2>\n\n\n\n<p>This item is very similar to item 10, just in other words. Don&#8217;t call  <code><strong>commit<\/strong>()<\/code> in helper functions or repository methods just because it feels convenient. That convenience becomes a maintenance problem quickly.<\/p>\n\n\n\n<p>Bad:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def create_user(session: AsyncSession, email: str) -&gt; User:\n    user = User(email=email)\n    session.add(user)\n    await session.commit()\n    return user\n\n\nasync def create_profile(session: AsyncSession, user: User) -&gt; Profile:\n    profile = Profile(user_id=user.id)\n    session.add(profile)\n    await session.commit()\n    return profile<\/code><\/pre>\n\n\n\n<p>Now a higher-level flow cannot treat both steps as one atomic operation.<\/p>\n\n\n\n<p>Better:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def create_user(session: AsyncSession, email: str) -&gt; User:\n    user = User(email=email)\n    session.add(user)\n    await session.flush()\n    return user\n\n\nasync def create_profile(session: AsyncSession, user: User) -&gt; Profile:\n    profile = Profile(user_id=user.id)\n    session.add(profile)\n    await session.flush()\n    return profile\n\n\n@app.post(\"\/users\")\nasync def create_user_endpoint(payload: UserCreate, session: SessionDep):\n    user = await create_user(session, payload.email)\n    await create_profile(session, user)\n    await session.commit()\n    await session.refresh(user)\n    return user<\/code><\/pre>\n\n\n\n<p>Benefits:<\/p>\n\n\n\n<ul>\n<li>the request or service layer owns the transaction<\/li>\n\n\n\n<li>multi-step workflows can succeed or fail as one unit<\/li>\n\n\n\n<li>helper functions become easier to compose and test<\/li>\n\n\n\n<li>rollback behavior is much clearer<\/li>\n<\/ul>\n\n\n\n<p>Low-level helpers should usually add objects, build statements, and maybe <code><strong>flush<\/strong>()<\/code>. The code that represents the full use case should decide when to <code><strong>commit<\/strong>()<\/code>. <\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Avoid-COUNT-When-You-Only-Need-To-Know-Whether-Something-Exists\">13. Avoid <code>COUNT(*)<\/code> When You Only Need To Know Whether Something Exists<\/h2>\n\n\n\n<p><code>COUNT(*)<\/code> is correct when you need the actual number. But for existence checks it is unnecessary work with cost.<\/p>\n\n\n\n<p>Bad:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">stmt = select(func.count()).select_from(Post).where(Post.slug == slug)\npost_count = await session.scalar(stmt)\nexists = post_count &gt; 0<\/code><\/pre>\n\n\n\n<p>Good:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import exists\n\nstmt = select(exists().where(Post.slug == slug))\npost_exists = await session.scalar(stmt)\n\n<\/code><\/pre>\n\n\n\n<p>Use <code><strong>COUNT<\/strong>(*)<\/code> for analytics, dashboards, and pagination totals. Do not use it as a habit for yes\/no checks.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"fastapi-sqlalchemy-Keep-Business-Logic-Close-To-The-Model\">14. Keep Business Logic in one place<\/h2>\n\n\n\n<p>Item 4 focused on building a reusable query API. This item focuses on something different: <strong>reusable domain behavior<\/strong>. Business rules related to a model should have one clear home. They should not be duplicated across route handlers, serializers, background jobs, CLI commands, and utility functions.<\/p>\n\n\n\n<p>For example, suppose creating a <code>Device<\/code> requires generating a serial number when one is not provided. Every code path that creates a device should use the same implementation of that rule. Otherwise, the behavior will eventually become inconsistent as the application evolves.<\/p>\n\n\n\n<p>Depending on the application&#8217;s architecture, this logic can live in a model classmethod, or in a repository method when database access is separated from domain models. The important part is not the specific layer. The important part is that the application exposes <strong>one clear entry point<\/strong> for the behavior. This follows several core engineering principles:<\/p>\n\n\n\n<ul>\n<li>DRY: the rule lives in one place<\/li>\n\n\n\n<li>single responsibility: routes focus on input\/output, not domain behavior<\/li>\n\n\n\n<li>encapsulation: the model manages its own valid state<\/li>\n\n\n\n<li>maintainability: future rule changes happen in one place<\/li>\n<\/ul>\n\n\n\n<p>In <strong>SQLAlchemy<\/strong>, the clean equivalent is usually repository method, or model classmethods that perform model-specific work without owning the final transaction boundary (remember Item 10 of the list).<\/p>\n\n\n\n<p>Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class Device(Base):\n    __tablename__ = \"devices\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n    name: Mapped[str] = mapped_column(String(100))\n    serial_number: Mapped[int] = mapped_column(unique=True, nullable=False)\n\n    @classmethod\n    async def create(\n        cls,\n        session: AsyncSession,\n        *,\n        name: str,\n        serial_number: int | None = None,\n    ) -&gt; \"Device\":\n        if serial_number is None:\n            max_serial_number = await session.scalar(select(func.max(cls.serial_number)))\n            serial_number = (max_serial_number or 0) + 1\n\n        device = cls(name=name, serial_number=serial_number)\n        session.add(device)\n        await session.flush()\n        return device<\/code><\/pre>\n\n\n\n<p>Then let a service own the use case:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def create_device_service(\n    session: AsyncSession,\n    payload: DeviceCreate,\n) -&gt; Device:\n    device = await Device.create(\n        session,\n        name=payload.name,\n        serial_number=payload.serial_number,\n    )\n    await session.commit()\n    await session.refresh(device)\n    return device<\/code><\/pre>\n\n\n\n<p>And again, keep the endpoint thin:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">@app.post(\"\/devices\")\nasync def create_device(payload: DeviceCreate, session: SessionDep):\n    return await create_device_service(session, payload)<\/code><\/pre>\n\n\n\n<p>In projects where SQLAlchemy models are used  primarily as persistence structures and keep database operations in repositories,  the same creation rule can live in a <code><strong>DeviceRepository<\/strong><\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import func, select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\n\nclass DeviceRepository:\n    def __init__(self, session: AsyncSession) -&gt; None:\n        self.session = session\n\n    async def create(\n        self,\n        *,\n        name: str,\n        serial_number: int | None = None,\n    ) -&gt; Device:\n        if serial_number is None:\n            serial_number = await self._next_serial_number()\n\n        device = Device(\n            name=name,\n            serial_number=serial_number,\n        )\n\n        self.session.add(device)\n        await self.session.flush()\n\n        return device\n\n    async def _next_serial_number(self) -&gt; int:\n        max_serial_number = await self.session.scalar(\n            select(func.max(Device.serial_number))\n        )\n        return (max_serial_number or 0) + 1<\/code><\/pre>\n\n\n\n<p>The service uses the repository while retaining control over the transaction:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">async def create_device_service(\n    session: AsyncSession,\n    payload: DeviceCreate,\n) -&gt; Device:\n    repository = DeviceRepository(session)\n\n    device = await repository.create(\n        name=payload.name,\n        serial_number=payload.serial_number,\n    )\n\n    await session.commit()\n    await session.refresh(device)\n\n    return device<\/code><\/pre>\n\n\n\n<p>Benefits are:<\/p>\n\n\n\n<ul>\n<li>every caller (API endpoints, CLI commands, tests, and background jobs) goes through the same rule and they stay consistent.<\/li>\n\n\n\n<li>services\/route handlers remain thinner and easier to reason about<\/li>\n\n\n\n<li>model behavior is easier to discover because it lives near the data it controls<\/li>\n<\/ul>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>The purpose of the above example is merely to show <strong>the business rule should not be scattered across callers<\/strong>. For cases where the rule depends on globally unique number generation under concurrency, <code>max(...) + 1<\/code> is usually not strong enough for high-traffic systems. In that case, prefer a database sequence, identity column, or another database-enforced allocation strategy. <\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"sqlalchemy-Use-with_for_update-Wisely\">15. Use <code>with_for_update()<\/code> Wisely<\/h2>\n\n\n\n<p>Locking is not just an ORM detail. It&#8217;s application behavior and it becomes part of your business behavior. The moment you introduce row locks, you are deciding whether other requests should wait, fail, retry, or skip work. In <strong>SQLAlchemy<\/strong>, <code><strong>with_for_update<\/strong>()<\/code> is one of the most useful tools for preventing race conditions, but it is also easy to misuse.<\/p>\n\n\n\n<p>Consider this example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy.orm import joinedload\n\nstmt = (\n    select(Order)\n    .options(joinedload(Order.customer), joinedload(Order.items))\n    .where(Order.id == order_id)\n    .with_for_update()\n)\norder = await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p>Depending on the backend and generated SQL, broad joins can widen the locking scope more than you expect.<\/p>\n\n\n\n<p>A safer pattern is to lock the base row explicitly and use narrower loading strategies:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy.orm import selectinload\n\nstmt = (\n    select(Order)\n    .options(selectinload(Order.items))\n    .where(Order.id == order_id)\n    .with_for_update(of=Order.__table__)\n)\norder = await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p>Two options matter a lot in real applications:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>nowait=True<\/code><\/h3>\n\n\n\n<p><code>nowait=True<\/code> means: try to acquire the lock immediately, and if another transaction already holds it, fail instead of waiting. It is a good fit when waiting several seconds would be worse than returning a clear conflict.<\/p>\n\n\n\n<p>Example use cases:<\/p>\n\n\n\n<ul>\n<li>an admin tries to edit a record that is currently locked by another operation<\/li>\n\n\n\n<li>an inventory reservation flow should fail fast and ask the client to retry<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy.orm import selectinload\n\nstmt = (\n    select(Order)\n    .options(selectinload(Order.items))\n    .where(Order.id == order_id)\n    .with_for_update(of=Order.__table__, nowait=True)\n)\norder = await session.scalar(stmt)<\/code><\/pre>\n\n\n\n<p>In this pattern, if the row is already locked, you can catch the database error and return something like HTTP <code>409 Conflict<\/code> instead of tying up the request.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>skip_locked=True<\/code><\/h3>\n\n\n\n<p><code>skip_locked=True<\/code> means: if a row is already locked by another transaction, do not wait and do not fail. Just skip that row and move on to other eligible rows.<\/p>\n\n\n\n<p>This is usually a better fit for worker-style flows than for user-facing endpoints.<\/p>\n\n\n\n<p>Example use cases:<\/p>\n\n\n\n<ul>\n<li>multiple background workers pulling jobs from the same table<\/li>\n\n\n\n<li>batch processors claiming the next available tasks<\/li>\n<\/ul>\n\n\n\n<p>Example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">stmt = (\n    select(Job)\n    .where(Job.status == \"pending\")\n    .order_by(Job.created_at.asc(), Job.id.asc())\n    .limit(10)\n    .with_for_update(skip_locked=True)\n)\njobs = (await session.scalars(stmt)).all()<\/code><\/pre>\n\n\n\n<p>This lets several workers claim different pending jobs in parallel without blocking each other.<\/p>\n\n\n\n<p>The trade-off is important: <code>skip_locked=True<\/code> can silently skip work that is currently locked, so it is great for queues but usually wrong for flows where the caller expects a specific row to be processed right now.<\/p>\n\n\n\n<p>Practical advice:<\/p>\n\n\n\n<ul>\n<li>lock as little as possible<\/li>\n\n\n\n<li>be careful when mixing row locks with eager joins<\/li>\n\n\n\n<li>keep the transaction short after acquiring the lock<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Enforce-Data-Consistency-at-the-Database-Level\">16. Enforce Data Consistency at the Database Level<\/h2>\n\n\n\n<p>Application-layer validation is important, but it is not sufficient to guarantee data consistency. Multiple API instances, direct database access, and race conditions can all bypass application-level checks. Critical constraints must be enforced by the database itself. <\/p>\n\n\n\n<p>Suppose your business rule requires <code>start_at<\/code> and <code>end_at<\/code> to occur on the same calendar day. A <strong>Pydantic <\/strong>input validation might look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from pydantic import BaseModel, model_validator\n\n\nclass CreateBookingRequest(BaseModel):\n    start_at: datetime\n    end_at: datetime\n\n    @model_validator(mode=\"after\")\n    def validate_same_day(self):\n        if self.start_at.date() != self.end_at.date():\n            raise ValueError(\n                \"start_at and end_at must be on the same day\"\n            )\n\n        return self<\/code><\/pre>\n\n\n\n<p><strong>Pydantic <\/strong>validation is very useful because it provides immediate, user-friendly feedback. However, it does not guarantee data integrity. Data can still be inserted through Database direct access, migrations, other services, or future code paths that forget to perform the validation. If the rule is truly required by the business domain, the database should enforce it as well.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import CheckConstraint\nfrom sqlalchemy.orm import Mapped, mapped_column\n\n\nclass Booking(Base):\n    __tablename__ = \"bookings\"\n\n    id: Mapped[int] = mapped_column(primary_key=True)\n\n    start_at: Mapped[datetime]\n    end_at: Mapped[datetime]\n\n    __table_args__ = (\n        CheckConstraint(\n            \"DATE(start_at) = DATE(end_at)\",\n            name=\"ck_booking_same_day\",\n        ),\n    )<\/code><\/pre>\n\n\n\n<p>Using the above constraint, now every write path must respect the rule, regardless of which application or user performs the insert. Catch the DB error regarding the constraint at appropriate place and raise domain-tailored Exception.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Make-Full-Use-of-Dependency-Injection-to-Keep-Routes-Clean-and-Thin\">17. Make Full Use of Dependency Injection to Keep Routes Clean and Thin<\/h2>\n\n\n\n<p>FastAPI dependencies are not only for authentication or database sessions. They are a great way to move request-scoped preparation out of route handlers: query parameter validation, pagination, settings, feature flags, tenant validation, and service construction. This keeps routes focused on orchestration instead of low-level work.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from typing import Annotated\n\nfrom fastapi import Depends, HTTPException, Query, status\nfrom pydantic import BaseModel, Field\n\n\nclass Pagination(BaseModel):\n    page: int = Field(default=1, ge=1)\n    size: int = Field(default=20, ge=1, le=100)\n\n    @property\n    def limit(self) -&gt; int:\n        return self.size\n\n    @property\n    def offset(self) -&gt; int:\n        return (self.page - 1) * self.size\n\n\ndef get_pagination(\n    page: Annotated[int, Query(ge=1)] = 1,\n    size: Annotated[int, Query(ge=1, le=100)] = 20,\n    settings: Settings = Depends(get_settings),\n) -&gt; Pagination:\n    if size &gt; settings.max_page_size:\n        raise HTTPException(\n            status_code=status.HTTP_400_BAD_REQUEST,\n            detail=f\"Page size cannot exceed {settings.max_page_size}\",\n        )\n\n    return Pagination(page=page, size=size)\n<\/code><\/pre>\n\n\n\n<p>The same idea applies to services. Instead of constructing services inside the route, create them through dependencies and inject already-prepared objects into the endpoint.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class ProductService:\n    def __init__(self, settings: Settings):\n        self.settings = settings\n\n    async def list_products(self, pagination: Pagination) -&gt; dict:\n        return {\n            \"source\": \"new-search\"\n            if self.settings.enable_new_search\n            else \"classic-search\",\n            \"limit\": pagination.limit,\n            \"offset\": pagination.offset,\n            \"items\": [\n                {\"id\": 1, \"name\": \"Keyboard\"},\n                {\"id\": 2, \"name\": \"Mouse\"},\n            ],\n        }\n\n\ndef get_product_service(\n    settings: Settings = Depends(get_settings),\n) -&gt; ProductService:\n    return ProductService(settings=settings)\n\n\n@app.get(\"\/v1\/products\")\nasync def list_products_v1(\n    tenant_id: str = Depends(validate_tenant_id),\n    pagination: Pagination = Depends(get_pagination),\n    flags: FeatureFlags = Depends(get_feature_flags),\n    service: ProductService = Depends(get_product_service),\n):\n    result = await service.list_products(pagination)\n\n    return {\n        \"tenant_id\": tenant_id,\n        \"features\": flags,\n        \"data\": result,\n    }\n<\/code><\/pre>\n\n\n\n<p>Here the route does almost no low-level work. It receives an already-validated <code>tenant_id<\/code>, a ready-to-use <code>Pagination<\/code> object, typed feature flags, and a configured service. The route becomes orchestration, dependencies prepare the request context, and services handle business logic. This also makes testing easier because FastAPI allows dependencies to be replaced with fakes through <code>app.dependency_overrides<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class FakeProductService:\n    async def list_products(self, pagination: Pagination) -&gt; dict:\n        return {\n            \"source\": \"fake\",\n            \"limit\": pagination.limit,\n            \"offset\": pagination.offset,\n            \"items\": [],\n        }\n\n\ndef get_fake_product_service() -&gt; FakeProductService:\n    return FakeProductService()\n\n\napp.dependency_overrides[get_product_service] = get_fake_product_service\n<\/code><\/pre>\n\n\n\n<p>So, don&#8217;t forget to use DI for every reusable component you need to create and use in routes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"Set-Explicit-Naming-Conventions-for-Database-Constraints\">18. Set Explicit Naming Conventions for Database Constraints<\/h2>\n\n\n\n<p>By default, SQLAlchemy generally does not assign names to primary-key, foreign-key, unique, or check constraints. If you do not name them, the database generates backend-specific names. Except index created with <code>index=True<\/code>, for which SQLAlchemy normally generates an <code>ix_...<\/code> name. Inconsistent or auto-generated names become a maintenance nightmare during migrations and debugging. SQLAlchemy\u2019s <code>MetaData<\/code> object accepts a <code>naming_convention<\/code> dictionary that automatically names all constraints in a consistent, readable way. Define this once on your base class:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">from sqlalchemy import MetaData\nfrom sqlalchemy.orm import DeclarativeBase\n\nconvention = {\n    \"ix\": \"ix_%(column_0_label)s\",\n    \"uq\": \"uq_%(table_name)s_%(column_0_name)s\",\n    \"ck\": \"ck_%(table_name)s_%(column_0_name)s\",\n    \"fk\": \"fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s\",\n    \"pk\": \"pk_%(table_name)s\",\n}\n\nclass Base(DeclarativeBase):\n    metadata = MetaData(naming_convention=convention)<\/code><\/pre>\n\n\n\n<p>SQLAlchemy applies these conventions to unnamed constraints, including constraints implicitly created through options such as <code data-start=\"1237\" data-end=\"1255\">primary_key=True<\/code>, <code data-start=\"1257\" data-end=\"1270\">unique=True<\/code>, and <code data-start=\"1276\" data-end=\"1288\">index=True<\/code>. Now every constraint gets a predictable name automatically:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">class User(Base):\n    __tablename__ = \"users\"\n    id: Mapped[int] = mapped_column(primary_key=True)\n    email: Mapped[str] = mapped_column(String(255), unique=True)  # uq_users_email\n    age: Mapped[int] = mapped_column(CheckConstraint(\"age &gt;= 18\"))  # ck_users_age\n\nclass Post(Base):\n    __tablename__ = \"posts\"\n    id: Mapped[int] = mapped_column(primary_key=True)  # pk_posts\n    user_id: Mapped[int] = mapped_column(ForeignKey(\"users.id\"))  # fk_posts_user_id_users<\/code><\/pre>\n\n\n\n<ul>\n<li><strong>Consistent Naming and<\/strong> <strong>no more guessing<\/strong>: When you see <code>fk_orders_customer_id_customers<\/code> in an error message, you immediately know which table, column, and referenced table are involved<\/li>\n\n\n\n<li><strong>Smoother migrations<\/strong>: Without your own convention, constraint names may be generated differently by PostgreSQL, MySQL, SQL Server, Oracle, or SQLite. A naming convention makes migrations deterministic and portable. Alembic uses the naming convention from the <code>target_metadata<\/code> configured in <code>env.py<\/code>:<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\"> from myapp.models import Base\n target_metadata = Base.metadata<\/code><\/pre>\n\n\n\n<p>We can use predictable constraint names for operations such as:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code lang=\"python\" class=\"language-python\">op.drop_constraint(\n     \"fk_orders_user_id_users\",\n     \"orders\",\n     type_=\"foreignkey\",\n )<\/code><\/pre>\n\n\n\n<ul>\n<li><strong>Easier debugging<\/strong>: Constraint violation errors become self-explanatory without cross-referencing your schema<\/li>\n<\/ul>\n\n\n\n<p>With this pattern, the database speaks in a language you understand. It&#8217;s a small setup cost that pays off every single time you run a migration or debug a constraint violation. Read <a href=\"https:\/\/alembic.sqlalchemy.org\/en\/latest\/naming.html\">this link<\/a> to learn more.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>These practices are not rigid laws. They are defaults that tend to keep FastAPI + SQLAlchemy codebases more readable, more predictable, less fragile under load and as Robert C. Martin said <em><strong>&#8220;to get it right&#8221;<\/strong><\/em>.<\/p>\n\n\n\n<p>The challenge is choosing patterns that remain understandable when the project grows. If a pattern makes your model layer harder to reason about, creates duplicate meanings in the data, or hides transaction behavior, it is usually worth stepping back and simplifying.<\/p>\n\n\n\n<p><strong>Happy coding!<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">References<\/h2>\n\n\n\n<ol>\n<li>SQLAlchemy 2.0 documentation: <a href=\"https:\/\/docs.sqlalchemy.org\/20\/\">https:\/\/docs.sqlalchemy.org\/20\/<\/a><\/li>\n\n\n\n<li>SQLAlchemy declarative mapping: <a href=\"https:\/\/docs.sqlalchemy.org\/20\/orm\/declarative_mapping.html\">https:\/\/docs.sqlalchemy.org\/20\/orm\/declarative_mapping.html<\/a><\/li>\n\n\n\n<li>SQLAlchemy session basics:<a href=\" https:\/\/docs.sqlalchemy.org\/20\/orm\/session_basics.html\"> https:\/\/docs.sqlalchemy.org\/20\/orm\/session_basics.html<\/a><\/li>\n\n\n\n<li>SQLAlchemy relationship loading: <a href=\"https:\/\/docs.sqlalchemy.org\/20\/orm\/queryguide\/relationships.html\">https:\/\/docs.sqlalchemy.org\/20\/orm\/queryguide\/relationships.html<\/a><\/li>\n\n\n\n<li>SQLAlchemy <code>with_for_update()<\/code> docs:<a href=\" https:\/\/docs.sqlalchemy.org\/20\/core\/selectable.html\"> https:\/\/docs.sqlalchemy.org\/20\/core\/selectable.html<\/a><\/li>\n\n\n\n<li>SQLAlchemy async ORM docs: <a href=\"https:\/\/docs.sqlalchemy.org\/20\/orm\/extensions\/asyncio.html\">https:\/\/docs.sqlalchemy.org\/20\/orm\/extensions\/asyncio.html<\/a><\/li>\n\n\n\n<li>FastAPI dependencies with <code>yield<\/code>: <a href=\"https:\/\/fastapi.tiangolo.com\/tutorial\/dependencies\/dependencies-with-yield\/\">https:\/\/fastapi.tiangolo.com\/tutorial\/dependencies\/dependencies-with-yield\/<\/a><\/li>\n\n\n\n<li>Django model best practices: <a href=\"https:\/\/mshaeri.com\/blog\/django-best-practices-part-1\/\">https:\/\/mshaeri.com\/blog\/django-best-practices-part-1\/<\/a><\/li>\n\n\n\n<li>Robert C. Martin, <em>Clean Architecture: A Craftsman&#8217;s Guide to Software Structure and Design<\/em>.<\/li>\n\n\n\n<li><a href=\"https:\/\/github.com\/zhanymkanov\/fastapi-best-practices#project-structure\">https:\/\/github.com\/zhanymkanov\/fastapi-best-practices#project-structure<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/alembic.sqlalchemy.org\/en\/latest\/naming.html\" data-type=\"link\" data-id=\"https:\/\/alembic.sqlalchemy.org\/en\/latest\/naming.html\">The Importance of Naming Constraints in SQLAlchemy<\/a><\/li>\n<\/ol>\n","protected":false},"excerpt":{"rendered":"<p>Most long-term problems, both in development and in production come from rushed model design, scattered logics, and patterns that feel convenient at first but become &hellip; <\/p>\n","protected":false},"author":1,"featured_media":2467,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,215,291,335,35,41],"tags":[97,344,220,350,339,338,131,340,293,342,336,348,341,343,346,39,347,43,345],"_links":{"self":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2391"}],"collection":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/comments?post=2391"}],"version-history":[{"count":4,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2391\/revisions"}],"predecessor-version":[{"id":2633,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/posts\/2391\/revisions\/2633"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/media\/2467"}],"wp:attachment":[{"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/media?parent=2391"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/categories?post=2391"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mshaeri.com\/blog\/wp-json\/wp\/v2\/tags?post=2391"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}