Skip to content
Skills Library
Programming · Building & coding

entity

/entity

Define a Protean entity, a domain object with unique identity that is part of an aggregate. Entities are similar to aggregates but don't manage their own persistence; they're always accessed through their parent aggregate. Use when adding objects with identity to an existing aggregate, when modeling concepts that have identity but are not standalone (like LineItem in Order, Comment in Post), when the user asks to "add an entity", "create a child object", or when they describe a concept that has identity but must be part of a larger aggregate. Entities cannot exist independently and are always

Repository
protean ↗
Licence
Apache-2.0
Last repository change
27.10.2026

Assessment and security

100/ 100Class: good
Format25 / 25
Frontmatter with name and description, valid name, description up to 1,024 characters, name matching the folder.
Discoverability25 / 25
Description explains when to use the skill, has a reasonable length and uses third person.
Content35 / 35
At least 150 words and at most 500 lines, headings, step lists, examples or code.
Hygiene15 / 15
No placeholders such as TODO, no Windows paths, with a licence.
Riskno signals detected in the scan
Files in this package
10
Scripts
4
HTML comments
0
URLs in SKILL.md
0
Deprecation

No signals detected. Repository changed on 27.10.2026, without references to retired models or APIs.

Licence

Apache-2.0, otwarta: można używać i zmieniać, z zachowaniem licencji i informacji o zmianach.

What to check before installing
  • 4 scripts in this package. Review them before installing, as Claude may execute them.

Scripts: assets/entity_nested.py, assets/entity_simple.py, assets/entity_with_hasmany.py, assets/entity_with_hasone.py

What it does and when to use it

Define a Protean entity, a domain object with unique identity that is part of an aggregate. Entities are similar to aggregates but don't manage their own persistence; they're always accessed through their parent aggregate. Use when adding objects with identity to an existing aggregate, when modeling concepts that have identity but are not standalone (like LineItem in Order, Comment in Post), when the user asks to "add an entity", "create a child object", or when they describe a concept that has identity but must be part of a larger aggregate. Entities cannot exist independently and are always

Triggers from the description

t manage their own persistence; theyadd an entitycreate a child object
Original author description

Define a Protean entity, a domain object with unique identity that is part of an aggregate. Entities are similar to aggregates but don't manage their own persistence; they're always accessed through their parent aggregate. Use when adding objects with identity to an existing aggregate, when modeling concepts that have identity but are not standalone (like LineItem in Order, Comment in Post), when the user asks to "add an entity", "create a child object", or when they describe a concept that has identity but must be part of a larger aggregate. Entities cannot exist independently and are always

How it works

  1. Basic structure
  2. Key rules
  3. Defining entities
  4. Associations
  5. Configuration options
  6. Working with entities
  7. Entity behavior
  8. Common mistakes
  9. Examples
  10. Detailed references
Original SKILL.md headings · 416 lines, 1,616 words
SKILL.md preview
---
name: entity
description: Define a Protean entity, a domain object with unique identity that is part of an aggregate. Entities are similar to aggregates but don't manage their own persistence; they're always accessed through their parent aggregate. Use when adding objects with identity to an existing aggregate, when modeling concepts that have identity but are not standalone (like LineItem in Order, Comment in Post), when the user asks to "add an entity", "create a child object", or when they describe a concept that has identity but must be part of a larger aggregate. Entities cannot exist independently and are always persisted through their aggregate.
license: Apache-2.0
compatibility: Requires Python 3.11+, protean framework
metadata:
  author: proteanhq
  version: "0.1"
  category: element
---

# Entity

An entity is an object with its own identity that lives inside an aggregate and is loaded and saved with it. This skill defines entities: their fields, associations, options and invariants.

## Basic structure

An entity is defined using the `@domain.entity` decorator and must specify which aggregate it belongs to:

```python
from protean import Domain
from protean.fields import Decimal, HasMany, Identifier, Integer

domain = Domain()

@domain.aggregate
class Order:
    customer_id: Identifier(required=True)
    line_items = HasMany("LineItem")

@domain.entity(part_of="Order")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True, min_value=1)
    unit_price: Decimal(precision=19, scale=4, required=True)
```

`customer_id` and `product_id` hold the ids of other aggregates. Link to another aggregate with an `Identifier` field, never with a `Reference`.

## Key rules

1. **Entities must be part of an aggregate**: Use `part_of` parameter to associate with parent aggregate. **Use a string reference** (`part_of="Order"`), as [Entity with part_of](#entity-with-part_of) explains. A nested entity names its parent entity (`part_of=LineItem` or `part_of="LineItem"`)
2. **Entities have automatic identity**: An `id` field is auto-generated unless `auto_add_id_field=False`
3. **Entities get automatic reference fields**: A reference field back to parent aggregate is created automatically
4. **Entities are accessed through aggregates**: Never query or update entities directly
5. **Entities are persisted with aggregates**: They use the same persistence store as their parent
6. **Entities can contain other entities**: Via `HasOne` or `HasMany` relationships
7. **Entities cannot be root elements**: They must always be part of an aggregate
8. **Entities are mutable**: Their state can change while maintaining identity

## Defining entities

### Simple entity

```python
from protean.fields import String

@domain.aggregate
class Post:
    title: String(required=True, max_length=200)
    comments = HasMany("Comment")

@domain.entity(part_of="Post")
class Comment:
    content: String(required=True, max_length=500)
    author: String(required=True, max_length=100)
    # Automatically gets: id field
    # Automatically gets: post = Reference(Post)
    # Automatically gets: post_id  # Shadow field holding the post's id
```

### Entity with part_of

**Use a string reference to the aggregate** for `part_of` to avoid circular dependencies. A nested entity names its parent entity instead (see [Nested entities](references/nested-entities.md)):

```python
@domain.entity(part_of="Order")
class LineItem:
    product_id: String(required=True)
    quantity: Integer(required=True)
```

**Why string references?** Aggregates, entities, and value objects are often defined in the same file. The aggregate references entities via `HasMany("LineItem")` (forward reference), and entities reference back via `part_of`. If entities use a class reference (`part_of=Order`), the entity must be defined *after* the aggregate, but the aggregate's `HasMany` needs the entity to exist too. Using string references on both sides (`HasMany("LineItem")` and `part_of="Order"`) breaks this circular dependency and allows elements to be defined in any order.

## Associations

### HasOne relationship

One-to-one relationship with another entity:

```python
@domain.aggregate
class Order:
    customer_id: Identifier(required=True)
    shipping_info = HasOne("ShippingInfo")

@domain.entity(part_of="Order")
class ShippingInfo:
    address: String(required=True, max_length=500)
    city: String(required=True, max_length=100)
```

**Note**: For `HasOne` relationships, access the related entity directly through the field (e.g., `order.shipping_info`). No special helper methods are needed.

### HasMany relationship

One-to-many relationship with other entities:

```python
@domain.aggregate
class Order:
    customer_id: Identifier(required=True)
    shipping_info = HasOne("ShippingInfo")
    line_items = HasMany("LineItem")

@domain.entity(part_of="Order")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True)
```

**Auto-generated helper methods**: When you define a `HasMany` field on an aggregate or entity, Protean automatically creates helper methods for managing the association. For the `line_items` field above, these methods are generated:

- `add_line_items(item)`: Add one or more items to the collection
- `remove_line_items(item)`: Remove an item from the collection
- `get_one_from_line_items(id=item_id)`: Get the one item that matches the keyword arguments. It raises `ObjectNotFoundError` (from `protean.exceptions`) when no item matches, and `TooManyObjectsError` when more than one does.
- `filter_line_items(**criteria)`: Return the items whose fields equal the given values, such as `filter_line_items(product_id="P1")`. It matches on equality only.

**Important**: Do NOT manually create these methods: they are automatically available. Only create custom methods if you need behavior different from the defaults.

### Automatic reference fields

Entities automatically get a reference field to their parent aggregate. A `LineItem` that is part of `Order` gets `item.order`, which returns the `Order` object, and `item.order_id`, which holds the order's id. [Accessing entities](#accessing-entities) shows both in use.

## Configuration options

### Sharing fields through a base class

`@domain.entity` has no `abstract` option. To share fields between entities, put them on a plain subclass of `BaseEntity` and leave it undecorated. Decorate only the concrete subclasses. Keep association fields (`HasMany`, `HasOne`) on the concrete entities, because two entities that inherit the same association fail `domain.init()`:

```python
from protean.core.entity import BaseEntity

class BaseLineItem(BaseEntity):
    quantity: Integer(required=True)

@domain.entity(part_of="Order")
class ProductLineItem(BaseLineItem):
    product_id: Identifier(required=True)
    # Inherits quantity from BaseLineItem
```

The base class is not registered with the domain, so it gets no table and no association. Do not decorate it: a decorated base becomes a real entity of its own.

### `auto_add_id_field`

Control automatic ID field generation:

```python
@domain.entity(part_of="Order", auto_add_id_field=False)
class LineItem:
    # You must provide your own identifier field
    line_number: Integer(required=True, identifier=True)
    product_id: Identifier(required=True)
```

### `schema_name`

Customize the persistence name:

```python
@domain.entity(part_of="Order", schema_name="order_items")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True)
```

## Working with entities

Creating instances needs an initialized domain and an active domain context, so the examples call `domain.init(traverse=False)` once and wrap their code in `with domain.domain_context():`. They use the auto-generated `add_line_items()` and `remove_line_items()` helpers, on this model:

```python
@domain.aggregate
class Order:
    customer_id: Identifier(required=True)
    line_items = HasMany("LineItem")

@domain.entity(part_of="Order")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True)
```

### Adding entities to aggregate

```python
domain.init(traverse=False)

with domain.domain_context():
    order = Order(customer_id="C123")

    # Create and add entity using auto-generated add_line_items() helper
    item = LineItem(product_id="P1", quantity=2)
    order.add_line_items([item])

    # Or add multiple
    items = [
        LineItem(product_id="P2", quantity=1),
        LineItem(product_id="P3", quantity=4),
    ]
    order.add_line_items(items)

    domain.repository_for(Order).add(order)
    order_id = order.id
```

### Accessing entities

```python
with domain.domain_context():
    # Access via aggregate
    order = domain.repository_for(Order).get(order_id)
    line_items = order.line_items  # List of LineItem entities

    # Iterate over entities
    for item in order.line_items:
        print(f"Product: {item.product_id}, Qty: {item.quantity}")

    # Access the parent from an entity
    parent_order = item.order  # Returns the Order object
    parent_id = item.order_id  # Returns the order's ID
```

### Updating entities

```python
with domain.domain_context():
    # Update through aggregate
    order = domain.repository_for(Order).get(order_id)
    order.line_items[0].quantity = 5  # Update entity
    domain.repository_for(Order).add(order)  # Save aggregate
```

### Removing entities

```python
with domain.domain_context():
    order = domain.repository_for(Order).get(order_id)
    item = order.line_items[0]
    order.remove_line_items(item)  # Uses auto-generated remove_line_items() helper
    domain.repository_for(Order).add(order)  # Save aggregate
```

## Entity behavior

### Methods and properties

Entities can have methods and computed properties:

```python
from decimal import Decimal as D  # stdlib Decimal, aliased so it does not clash with the field

@domain.entity(part_of="Order")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True, min_value=1)
    unit_price: Decimal(precision=19, scale=4, required=True)

    @property
    def subtotal(self) -> D:
        """Calculate line item subtotal."""
        return self.quantity * self.unit_price

    def increase_quantity(self, amount: int):
        """Increase quantity by specified amount."""
        if amount <= 0:
            raise ValueError("Amount must be positive")
        self.quantity += amount
```

### Validation and invariants

Entities support three levels of validation (use in this order):

**1. Field-level constraints**: For data type validations and simple constraints.

**Important**: Always define data type validations at the field level using built-in parameters. Never validate these manually in methods.

```python
@domain.entity(part_of="Order")
class LineItem:
    product_id: String(required=True, max_length=50, min_length=3)
    quantity: Integer(required=True, min_value=1, max_value=1000)
    unit_price: Decimal(precision=19, scale=4, required=True, min_value=0.01)
    discount_percent: Float(min_value=0.0, max_value=100.0, default=0.0)
```

**Common field validation parameters**:
- **All fields**: `required`, `default`, `unique`, `choices`, `validators`
- **String**: `max_length`, `min_length`, `sanitize`
- **Integer/Float/Decimal**: `min_value`, `max_value`
- **Decimal**: `precision`, `scale` (use `Decimal(precision=19, scale=4)` for money)

**2. Business rules as invariants**: For cross-field validations and domain logic, one business rule per invariant.

```python
from protean import invariant
from protean.exceptions import ValidationError

@domain.entity(part_of="Order")
class LineItem:
    product_id: String(required=True, max_length=50)
    quantity: Integer(required=True, min_value=1)
    unit_price: Decimal(precision=19, scale=4, required=True, min_value=0.01)
    discount_percent: Float(min_value=0.0, max_value=100.0, default=0.0)

    @invariant.post
    def large_discount_needs_bulk_quantity(self):
        """Granular business rule: discounts above 20% need 10 or more units.

        Post invariants run at the end of __init__ and on every field assignment.
        """
        if self.discount_percent > 20 and self.quantity < 10:
            raise ValidationError(
                {"_entity": ["Discounts above 20% need a quantity of 10 or more"]}
            )

    @invariant.post
    def quantity_times_price_must_be_reasonable(self):
        """Granular business rule: total price sanity check.

        Keep each invariant focused on ONE business constraint.
        """
        total = self.quantity * self.unit_price
        if total > 1_000_000:  # Business limit
            raise ValidationError(
                {"_entity": ["Line item total exceeds maximum allowed"]}
            )

    @property
    def subtotal(self) -> D:
        """Calculate line item subtotal."""
        base = self.quantity * self.unit_price
        discount = base * D(str(self.discount_percent)) / 100
        return base - discount
```

**Note**: Entity invariants are validated as part of the aggregate's validation. Post-invariants run at the end of `__init__` and on every field assignment. A change to a child entity also runs the aggregate root's post-invariants. `repository.add()` does not check them again, so an invalid change fails at the assignment, not at save time.

## Common mistakes

❌ **Querying entities directly**
```python
# fragment
# Don't do this
comments = domain.repository_for(Comment).filter(author="John")
```

✅ **Access through aggregate**: Load the aggregate and use its helper, `post.filter_comments(author="John")`.

---

❌ **Entity containing another entity without relationship**
```python
# fragment
@domain.entity(part_of="Order")
class LineItem:
    note = Comment()  # Wrong: Use HasOne relationship
```

✅ **Use proper association fields**:
```python
@domain.aggregate
class Order:
    customer_id: Identifier(required=True)
    line_items = HasMany("LineItem")

@domain.entity(part_of="Order")
class LineItem:
    product_id: Identifier(required=True)
    quantity: Integer(required=True)
    notes = HasMany("LineItemNote")

@domain.entity(part_of=LineItem)
class LineItemNote:
    content: String(max_length=500)
```

A nested entity is `part_of` its parent entity, not the aggregate. Pass the parent entity as a class or as a string.

---

❌ **Using entity as aggregate root**
```python
# fragment
@domain.entity(part_of="Order")
class LineItem:
    product_id: String(required=True)

# Don't persist entities directly
domain.repository_for(LineItem).add(item)  # Wrong!
```

✅ **Persist through aggregate**: Add the entity to its aggregate, as in [Adding entities to aggregate](#adding-entities-to-aggregate), and persist the aggregate.

## Examples

- [Simple entity](assets/entity_simple.py): a minimal entity inside an aggregate

## Detailed references

- [HasOne relationships](references/with-hasone.md): One-to-one entity relationships
- [HasMany relationships](references/with-hasmany.md): One-to-many entity relationships
- [Nested entities](references/nested-entities.md): Entities containing other entities
- [Configuration options](references/configuration.md): Entity configuration and customization
- [Anti-patterns](references/anti-patterns.md): Common mistakes and how to avoid them

## Related skills

- [aggregate](../aggregate/SKILL.md): Learn about aggregates that contain entities
- [value-object](../value-object/SKILL.md): When to use value objects instead of entities

## Verify your work

- [Verify with check](../../references/verify-with-check.md): run `check` and resolve what it reports
Files in this package (10)
  • SKILL.md 15 KB
  • assets/entity_nested.py 9 KB
  • assets/entity_simple.py 3 KB
  • assets/entity_with_hasmany.py 7 KB
  • assets/entity_with_hasone.py 5 KB
  • references/anti-patterns.md 10 KB
  • references/configuration.md 6 KB
  • references/nested-entities.md 10 KB
  • references/with-hasmany.md 7 KB
  • references/with-hasone.md 5 KB

Versions

The database has no other skills named entity.

Identical content appears in 1 other GitHub folders. We show one copy with the highest score.

Database snapshot: 13.11.2026 · package version 683e92ca853d (SHA-256 digest of the entire package)

Downloads in the last 30 days: 0 · ranking

Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.

Similar skills

  • 100aggregate
    Define a Protean aggregate, the root entity of a cluster of domain objects treated as a single unit for data changes. Aggregates are fundamental building blocks that encapsulate business logic, enforce invariants, and define transaction boundaries. Use when creating domain…
  • 100value-object
    Define a Protean value object, an immutable domain element without identity, defined by its attributes. Value objects represent descriptive concepts like Money, Email, Address, or Coordinates. Use when modeling concepts where identity doesn't matter, only the values do. Use when…
  • 100add-entity
    Add a domain entity/aggregate with EF configuration and a migration to an existing FSH module. Use when adding a new database-backed entity. Pairs with add-feature and create-migration.
  • 96ddd-aggregate
    Scaffold an aggregate root with entity, value objects, repository interface, domain events, and test stubs. Use when adding a new aggregate to an existing bounded context, modeling a new business concept that owns invariants, or generating the boilerplate for an entity + repo + events triplet.
  • 90create-persistence-adapter
    Create or update the JPA persistence adapter for one domain aggregate: entity, Spring Data repository, mapper, the @Component implementing the outbound Repository port, its Flyway migration and its @DataJpaTest. Use when the user asks to create a persistence adapter, persist an…
  • 90create-persistence-adapter
    Create or update the JPA persistence adapter for one domain aggregate: entity, Spring Data repository, mapper, the @Component implementing the outbound Repository port, its Flyway migration and its @DataJpaTest. Use when the user asks to create a persistence adapter, persist an…

Feedback and reports

Your message goes to Bartosz Sokoliński. An email address is needed only for a reply. See the privacy policy.