Core Principles
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
- Start with Domain: Define aggregates first
- Use Events: All state changes generate events
- Separate Commands/Queries: Use CQRS pattern
- Test Aggregates: Test business logic in isolation
- Type Everything: Use type hints throughout
In Your Architecture
- Clear Boundaries: Define bounded contexts
- Event-Driven: Use events for communication
- Scalable: Design for independent scaling
- Observable: Log events and state changes
- Maintainable: Keep code clean and organized
Next Steps
- Learn about Architecture
- Explore Core Concepts
- Start building with Getting Started