Skip to content

Hello, Protean!

Define an aggregate, save it, load it — in under 20 lines of Python.

Prerequisites

The Code

Create a file called hello.py:

from protean import Domain
from protean.fields import Boolean, String

domain = Domain()


@domain.aggregate
class Task:
    title: String(max_length=100, required=True)
    done: Boolean(default=False)


domain.init(traverse=False)

if __name__ == "__main__":
    with domain.domain_context():
        # Create
        task = Task(title="Buy groceries")
        print(f"Created: {task.title} (done={task.done})")

        # Save
        repo = domain.repository_for(Task)
        repo.add(task)

        # Load
        saved = repo.get(task.id)
        print(f"Loaded:  {saved.title} (done={saved.done})")
        print(f"ID:      {saved.id}")

Run it:

$ python hello.py
Created: Buy groceries (done=False)
Loaded:  Buy groceries (done=False)
ID:      5eb04301-f191-4bca-9e49-8e5a948f07f6

The ID will differ on your machine — Protean generates a unique identifier for every aggregate instance automatically.

What Just Happened?

Three things:

  1. You defined an aggregate. Task is a domain concept — a cluster of data and rules treated as a single unit. The @domain.aggregate decorator registers it with the domain.

  2. You saved it. repository_for(Task) gives you a repository — a persistence abstraction. The default in-memory adapter stores everything in a dictionary. No database required.

  3. You loaded it back. repo.get(task.id) retrieves the task by its auto-generated ID. Everything round-trips cleanly.

All of this ran in-memory. No database, no configuration, no boilerplate. When you are ready for a real database, you swap in an adapter through configuration — your domain code stays exactly the same.

Next Steps

Ready for more? The Quickstart builds a complete domain with commands, events, and handlers in 5 minutes.

Or dive into the Tutorial for a guided, chapter-by-chapter journey from aggregates to production.