Skip to main content

Core Principles

info

Fluvius Framework is built on proven principles from Domain-Driven Design, Event Sourcing, and modern software architecture.

1. Domain-Driven Design

Business Logic First

The domain model is the heart of your application. Business logic lives in aggregates, not in services or controllers.

Principle:

  • Business rules are enforced in aggregates
  • Domain logic is isolated from infrastructure
  • Code reflects business terminology

Example:

class AccountAggregate(Aggregate):
@action(evt_key='account-opened')
async def open_account(self, customer_id: str, initial_balance: float):
# Business rule: minimum balance required
if initial_balance < 100:
raise ValueError("Minimum balance is $100")
# Business logic here
return {'customer_id': customer_id, 'balance': initial_balance}

Ubiquitous Language

Use the same terminology in code and business conversations.

Principle:

  • Code uses business terms
  • Domain experts understand the code
  • No translation layer needed

Example:

# Good: Uses business language
@action(evt_key='order-placed')
async def place_order(self, items: list):
pass

# Bad: Uses technical terms
@action(evt_key='order-created')
async def create_order(self, products: list):
pass

Bounded Contexts

Clear boundaries between different parts of the system.

Principle:

  • Each domain is a bounded context
  • Domains can evolve independently
  • Clear interfaces between contexts

2. Event Sourcing

Events as Facts

Events represent things that have happened. They are immutable facts.

Principle:

  • Events are append-only
  • Events cannot be changed
  • Events can be replayed

Example:

@action(evt_key='user-created', resources=['user'])
async def create_user(self, name: str, email: str):
# Event 'user-created' is automatically generated
# It's immutable and stored forever
return {'name': name, 'email': email}

Event Log as Source of Truth

The event log is the single source of truth. Current state is derived from events.

Principle:

  • All state changes are events
  • State can be rebuilt from events
  • Complete history is preserved

Time Travel

Query state at any point in time by replaying events.

Principle:

  • Replay events up to a point
  • Query historical state
  • Debug by replaying events

3. Command Query Responsibility Segregation (CQRS)

Separate Commands and Queries

Commands modify state, queries read state. They use different models.

Principle:

  • Commands go through aggregates
  • Queries read from state store
  • Different models for reads and writes

Example:

# Command: Modify state
command = domain.create_command('create-user', {...})
response = await domain.process_command(command)

# Query: Read state
user = await domain.statemgr.fetch('user', user_id)

Independent Scaling

Scale read and write operations independently.

Principle:

  • Read replicas for queries
  • Separate write instances
  • Optimize each path independently

4. Immutability

Immutable Events

Events are immutable. Once created, they never change.

Principle:

  • Events are append-only
  • No updates or deletes
  • Compensating events for corrections

Example:

# Good: Create new event for correction
@action(evt_key='user-email-corrected')
async def correct_email(self, new_email: str):
# Original event remains, new event corrects it
pass

# Bad: Trying to modify existing event
# This is not possible in Fluvius

Immutable State Snapshots

State snapshots are point-in-time views.

Principle:

  • State is derived from events
  • Snapshots are read-only
  • New state comes from new events

5. Explicit State Management

State is Explicit

State is managed explicitly, not hidden in databases.

Principle:

  • State is defined in aggregates
  • State transitions are explicit
  • State changes generate events

Example:

class UserState(DataModel):
name: str = field()
email: str = field()
active: bool = field(initial=True)

class UserAggregate(Aggregate):
def __init__(self, domain):
super().__init__(domain)
self._state = UserState()

@action(evt_key='user-created')
async def create_user(self, name: str, email: str):
# Explicit state update
self._state = UserState(name=name, email=email, active=True)
return self._state

6. Testability

Test in Isolation

Aggregates can be tested without infrastructure.

Principle:

  • Test business logic separately
  • Mock infrastructure dependencies
  • Test events and state changes

Example:

async def test_create_user():
aggregate = UserAggregate(domain)
result = await aggregate.create_user('John', 'john@example.com')
assert result['name'] == 'John'
# Verify event was generated

7. Type Safety

Type Hints Throughout

Full type hint support for better IDE support and error detection.

Principle:

  • Type hints on all methods
  • Runtime validation with Pydantic
  • Better IDE autocomplete

Example:

from typing import Optional

class UserAggregate(Aggregate):
@action(evt_key='user-created')
async def create_user(self, name: str, email: str) -> dict:
return {'name': name, 'email': email}

8. Convention over Configuration

Sensible Defaults

Sensible defaults reduce boilerplate.

Principle:

  • Defaults work for most cases
  • Override when needed
  • Less configuration required

Example:

# Defaults work out of the box
class UserDomain(Domain):
__aggregate__ = UserAggregate
# No need to configure event store, state manager, etc.

9. Modularity

Modular Architecture

Framework is built from independent modules.

Principle:

  • Use only what you need
  • Modules are independent
  • Easy to extend

Example:

# Use only domain module
from fluvius.domain import Domain

# Or add FastAPI integration
from fluvius.fastapi import FluviusFastAPI

# Or add worker support
from fluvius.worker import Worker

10. Developer Experience

Easy to Use

Framework should be easy to use and understand.

Principle:

  • Clear API
  • Good documentation
  • Helpful error messages
  • Good tooling

Applying These Principles

In Your Code

  1. Start with Domain: Define aggregates first
  2. Use Events: All state changes generate events
  3. Separate Commands/Queries: Use CQRS pattern
  4. Test Aggregates: Test business logic in isolation
  5. Type Everything: Use type hints throughout

In Your Architecture

  1. Clear Boundaries: Define bounded contexts
  2. Event-Driven: Use events for communication
  3. Scalable: Design for independent scaling
  4. Observable: Log events and state changes
  5. Maintainable: Keep code clean and organized

Next Steps