Skip to content

Models & Relationships

Models are the data structures that represent your API’s entities and their relationships. They serve as the foundation for interacting with databases, validating data, and exchanging information between your API endpoints and business logic.

Each model must be derived from the Entity base class, which provides core functionalities and integration with the Atlas service and repository layers.

Below are the details on how to create and customize a model.

Initializing the models package

Before creating models, create the src/models directory. This will be the Python package where all the API's models will be stored. Also, create a __init__.py file inside it for convenient importing.

Example:

src/models/__init__.py
from .product import Product
from .category import Category
from .price import Price
from .stock import Stock
from .user import User
from .admin import Admin
from .customer import Customer

__all__ = [
    "Product",
    "Category",
    "Price",
    "Stock",
    "User",
    "Admin",
    "Customer"
]

Creating a Model

To create a model, follow the steps below:

  • Create a file inside src/models with the name of the model in snake_case;
  • Declare the model's class by extending Entity and naming it with the same name as the file, but in PascalCase;
  • Declare the internal variable __tablename__ — this will be the name of the model's table in the database (use snake_case plural names);
  • Add an entry for your model in src/models/__init__.py;
  • Declare your fields using SQLAlchemy's resources and syntax, such as Mapped and mapped_column;

Done. Your model (or changes in its columns) will be included in the next database migration.

Example:

src/models/product.py
from typing import Optional

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

Relationships

Your API’s structure will likely include multiple models interacting through different types of data relationships.

Note that for importing other entities inside an entity, you will need to use forward references (using class names as strings instead of actual class references) and TYPE_CHECKING (imports at the top and bottom of your model file — for avoiding problems with circular imports and language servers, e.g. Pylance).

Below is an overview of each relationship type and how to define them properly.

1:N — Bidirectional

A bidirectional 1:N (one-to-many) relationship links a single parent to multiple children, while each child also points back to that parent. In SQLAlchemy, both sides expose a relationship() connected through back_populates, keeping parent and child in sync.

Example: Product 1:N Price

src/models/product.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .price import Price

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # OneToMany
    prices: Mapped[Optional[List["Price"]]] = relationship(
        "Price",
        foreign_keys="Price.product_uuid",
        cascade="all, delete-orphan"
        back_populates="product"
    )

from .price import Price
src/models/price.py
from typing import TYPE_CHECKING, Optional

from sqlalchemy import String, Float, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .product import Product

class Price(Entity):
    __tablename__ = "prices"

    value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    previous_value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    currency: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys=[product_uuid],
        back_populates="prices"
    )

from .product import Product

1:N — Unidirectional, Omitted Field in Side 1 (Only Children Can Access Parent)

In a unidirectional 1:N relationship with the field omitted on the 1 side, only the child objects maintain a reference to the parent. The parent does not expose a collection of its children. This pattern keeps the model simpler when navigation is only required from child to parent, and the ORM does not synchronize changes in the opposite direction.

Example: Product 1:N Price

src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # "prices" omitted
src/models/price.py
from typing import TYPE_CHECKING, Optional

from sqlalchemy import String, Float, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .product import Product

class Price(Entity):
    __tablename__ = "prices"

    value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    previous_value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    currency: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys=[product_uuid]
    ) # back_populates="prices" omitted

from .product import Product

1:N — Unidirectional, Omitted Field in Side N (Only Parent Can Access Children)

In a unidirectional 1:N relationship with the field omitted on the N side, only the parent exposes a collection of related children. The children store the foreign key but do not define a relationship back to the parent. This pattern is useful when navigation is required solely from parent to children, while still preserving referential integrity at the database level.

Example: Product 1:N Price

src/models/product.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .price import Price

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # OneToMany
    prices: Mapped[Optional[List["Price"]]] = relationship(
        "Price",
        foreign_keys="Price.product_uuid",
        cascade="all, delete-orphan"
    ) # back_populates="product" omitted

from .price import Price
src/models/price.py
from typing import Optional

from sqlalchemy import String, Float, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Price(Entity):
    __tablename__ = "prices"

    value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    previous_value: Mapped[Optional[float]] = mapped_column(Float, default=0)
    currency: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    # "product" omitted; keep only the foreign key (e.g. "product_uuid")

1:1 — Bidirectional, a Constrained Variant of 1:N

A bidirectional 1:1 relationship works like a 1:N with a uniqueness constraint on the child, ensuring only one child per parent. Parent and child roles remain the same, and the same omission rules from the unidirectional 1:N cases apply. Both sides expose a relationship() connected through back_populates.

Example: User 1:1 Admin

src/models/user.py
from typing import TYPE_CHECKING, Optional

from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .admin import Admin

class User(Entity):
    __tablename__ = "users"

    name: Mapped[Optional[str]] = mapped_column(String)
    email: Mapped[Optional[str]] = mapped_column(String)
    username: Mapped[Optional[str]] = mapped_column(String)
    password: Mapped[Optional[str]] = mapped_column(String)

    # OneToOne
    admin: Mapped[Optional["Admin"]] = relationship(
        "Admin",
        foreign_keys="Admin.user_uuid",
        uselist=False,
        back_populates="user"
    )

from .admin import Admin
src/models/admin.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

if TYPE_CHECKING:
    from .user import User

class Admin(Entity):
    __tablename__ = "admins"

    # OneToOne
    user_uuid: Mapped[str] = mapped_column(ForeignKey("users.uuid"), unique=True)
    user: Mapped["User"] = relationship(
        "User",
        foreign_keys=[user_uuid],
        cascade="all, delete-orphan"
        uselist=False,
        single_parent=True,
        back_populates="admin"
    )

from .user import User

N:N — Bidirectional

A bidirectional N:N relationship connects two models through an association table, allowing each side to reference multiple records from the other. In SQLAlchemy, both models define a relationship() using the same secondary table and link to each other via back_populates, enabling navigation in both directions. Create the relational tables in src/models/__relationships__.py using the pattern entity1_rel_entity2 ("rel" is for "relationship").

Example: Product N:N Category

src/models/__relationships__.py
from sqlalchemy import Table, Column, ForeignKey
from atlas.core import Entity

products_rel_categories = Table(
    "products_rel_categories",
    Entity.metadata,
    Column("product_uuid", ForeignKey("products.uuid"), primary_key=True),
    Column("category_uuid", ForeignKey("categories.uuid"), primary_key=True)
)
src/models/product.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import products_rel_categories

if TYPE_CHECKING:
    from .category import Category

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToMany
    categories: Mapped[Optional[List["Category"]]] = relationship(
        "Category",
        secondary=products_rel_categories,
        back_populates="products"
    )

from .category import Category
src/models/category.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import products_rel_categories

if TYPE_CHECKING:
    from .product import Product

class Category(Entity):
    __tablename__ = "categories"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToMany
    products: Mapped[Optional[List["Product"]]] = relationship(
        "Product",
        secondary=products_rel_categories,
        back_populates="categories"
    )

from .product import Product

N:N — Unidirectional

A unidirectional N:N relationship exposes the association only from one model. The parent defines the relationship() using the shared secondary table, while the other side omits its counterpart. This keeps navigation one-way, but the underlying many-to-many link still exists and is fully enforced at the database level. Create the relational tables in src/models/__relationships__.py using the pattern entity1_rel_entity2 ("rel" is for "relationship").

Example: Product N:N Category

src/models/__relationships__.py
from sqlalchemy import Table, Column, ForeignKey
from atlas.core import Entity

products_rel_categories = Table(
    "products_rel_categories",
    Entity.metadata,
    Column("product_uuid", ForeignKey("products.uuid"), primary_key=True),
    Column("category_uuid", ForeignKey("categories.uuid"), primary_key=True)
)
src/models/product.py
from typing import TYPE_CHECKING, Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import products_rel_categories

if TYPE_CHECKING:
    from .category import Category

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToMany
    categories: Mapped[Optional[List["Category"]]] = relationship(
        "Category",
        secondary=products_rel_categories
    ) # back_populates="products" omitted

from .category import Category
src/models/category.py
from typing import Optional, List

from sqlalchemy import String, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import products_rel_categories

class Category(Entity):
    __tablename__ = "categories"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # "products" omitted

Self Relationships

Your API may include models that relate not only to other models, but also to themselves. These self-referential relationships are commonly used to represent hierarchical or associative structures—such as parent/child trees, bidirectional links, or many-to-many associations within the same table.

Note that for importing other entities inside an entity, you will need to use forward references (using class names as strings instead of actual class references) and TYPE_CHECKING (imports at the top and bottom of your model file — for avoiding problems with circular imports and language servers, e.g. Pylance).

Below is an overview of each type of self relationship and how to define them correctly in SQLAlchemy.

Self 1:N — Bidirectional

A bidirectional self 1:N relationship models a hierarchical structure within the same table, where each record can reference a single parent and also expose a collection of its children. In SQLAlchemy, this pattern requires remote_side to clarify which side represents the parent, and both directions are kept synchronized through back_populates.

Example: Product 1:N Product

src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    parent_product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    parent_product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys=[parent_product_uuid],
        remote_side="Product.uuid",
        back_populates="child_products"
    )

    # OneToMany
    child_products: Mapped[Optional[List["Product"]]] = relationship(
        "Product",
        foreign_keys=[parent_product_uuid],
        back_populates="parent_product"
    )

Self 1:N — Unidirectional, Omitted Field in Side 1 (Only Children Can Access Parent)

In a unidirectional self 1:N relationship with the field omitted on the 1 side, each record can access its parent but the parent does not expose a list of children. The model preserves the hierarchy through the foreign key, and remote_side identifies the parent, but navigation is intentionally restricted to the child → parent direction.

Example: Product 1:N Product

src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    parent_product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    parent_product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys=[parent_product_uuid],
        remote_side="Product.uuid"
    ) # back_populates="child_products" omitted

    # "child_products" omitted

Self 1:N — Unidirectional, Omitted Field in Side N (Only Parent Can Access Children)

In a unidirectional self 1:N relationship with the field omitted on the N side, the parent exposes its children but each child does not define a relationship back to the parent. The hierarchy is still enforced through the foreign key, and SQLAlchemy manages the collection on the parent side without maintaining any reverse navigation on the child.

Example: Product 1:N Product

src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToOne
    parent_product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"))
    # "parent_product" omitted; keep only the foreign key (e.g. "parent_product_uuid")

    # OneToMany
    child_products: Mapped[Optional[List["Product"]]] = relationship(
        "Product",
        foreign_keys=[parent_product_uuid]
    ) # back_populates="parent_product" omitted

Self 1:1 — Bidirectional, a Constrained Variant of 1:N

A bidirectional self 1:1 relationship is a restricted form of a self 1:N, where the foreign key on the child is marked as unique so that each record can have only one parent and one child. As in any self-relationship, remote_side identifies the parent side, and both directions are exposed through back_populates, allowing full navigation between the two linked records.

Example: Product 1:1 Product

src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # OneToOne
    parent_product_uuid: Mapped[Optional[str]] = mapped_column(ForeignKey("products.uuid"), unique=True)
    parent_product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys=[parent_product_uuid],
        remote_side="Product.uuid",
        uselist=False,
        single_parent=True,
        back_populates="child_product"
    )

    # OneToOne
    child_product: Mapped[Optional["Product"]] = relationship(
        "Product",
        foreign_keys="Product.parent_product_uuid",
        uselist=False,
        back_populates="parent_product"
    )

Self N:N — Bidirectional

A bidirectional self N:N relationship links records within the same table through an association table, allowing each entry to reference multiple peers and be referenced by them in return. Both sides define complementary relationship() configurations using the same secondary table, enabling full navigation in both directions.

Example: Product N:N Product

src/models/__relationships__.py
from sqlalchemy import Table, Column, ForeignKey
from atlas.core import Entity

a_products_rel_b_products = Table(
    "a_products_rel_b_products",
    Entity.metadata,
    Column("a_product_uuid", ForeignKey("products.uuid"), primary_key=True),
    Column("b_product_uuid", ForeignKey("products.uuid"), primary_key=True)
)
src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import a_products_rel_b_products

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToMany
    a_products = relationship(
        "Product", 
        secondary=a_products_rel_b_products, 
        primaryjoin="Product.uuid == a_products_rel_b_products.c.a_product_uuid",
        secondaryjoin="Product.uuid == a_products_rel_b_products.c.b_product_uuid",
        back_populates="b_products"
    )

    # ManyToMany
    b_products = relationship(
        "Product", 
        secondary=a_products_rel_b_products, 
        primaryjoin="Product.uuid == a_products_rel_b_products.c.b_product_uuid",
        secondaryjoin="Product.uuid == a_products_rel_b_products.c.a_product_uuid",
        back_populates="a_products"
    )

Self N:N — Unidirectional

A unidirectional self N:N relationship exposes the many-to-many link only from one side of the model. The association table still defines the full connection between records of the same table, but only one relationship is declared in the ORM, restricting navigation to a single direction while preserving all database-level semantics.

Example: Product N:N Product

src/models/__relationships__.py
from sqlalchemy import Table, Column, ForeignKey
from atlas.core import Entity

a_products_rel_b_products = Table(
    "a_products_rel_b_products",
    Entity.metadata,
    Column("a_product_uuid", ForeignKey("products.uuid"), primary_key=True),
    Column("b_product_uuid", ForeignKey("products.uuid"), primary_key=True)
)
src/models/product.py
from typing import Optional, List

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship

from atlas.core import Entity
from .__relationships__ import a_products_rel_b_products

class Product(Entity):
    __tablename__ = "products"

    name: Mapped[Optional[str]] = mapped_column(String)
    slug: Mapped[Optional[str]] = mapped_column(String)
    sku: Mapped[Optional[str]] = mapped_column(String)
    barcode: Mapped[Optional[str]] = mapped_column(String)
    description: Mapped[Optional[str]] = mapped_column(String)
    short_description: Mapped[Optional[str]] = mapped_column(String)

    # ManyToMany
    a_products = relationship(
        "Product", 
        secondary=a_products_rel_b_products, 
        primaryjoin="Product.uuid == a_products_rel_b_products.c.a_product_uuid",
        secondaryjoin="Product.uuid == a_products_rel_b_products.c.b_product_uuid",
    ) # back_populates="b_products" omitted

    # "b_products" omitted