CQRS Design Architecture¶
Last Updated: 2025-12-30 Status: Complete Audience: Backend developers, database designers
Overview¶
FraiseQL implements the CQRS (Command Query Responsibility Segregation) pattern, separating read operations (queries) from write operations (mutations) for optimal performance and maintainability.
CQRS Pattern in FraiseQL¶
flowchart TD
A[GraphQL Request] --> B{Operation Type?}
B -->|Query| C[Read Path]
B -->|Mutation| D[Write Path]
C --> E[PostgreSQL Views]
D --> F[PostgreSQL Functions]
E --> G[Optimized for Reading]
F --> H[Optimized for Writing]
G --> I[JSONB Pre-formatted]
H --> J[Business Logic]
I --> K[Rust Pipeline]
J --> L[Validation & Constraints]
K --> M[Fast Response]
L --> N[Reliable Updates]
style C fill:#e8f5e9
style D fill:#fff3e0
style E fill:#c8e6c9
style F fill:#ffe0b2
Key Principle: Different data structures for reading vs writing leads to better performance and clearer code.
Query Path (Read Operations)¶
View-Based Queries¶
flowchart LR
A[GraphQL Query] --> B[FraiseQL Schema]
B --> C[SELECT from View]
C --> D[(v_user)]
D --> E[JSONB Result]
E --> F[Rust Pipeline]
F --> G[JSON Response]
style D fill:#e8f5e9
style F fill:#c8e6c9
PostgreSQL View Example:
-- Read-optimized view
CREATE VIEW v_user AS
SELECT
id, -- Public UUID
jsonb_build_object(
'id', id,
'name', name,
'email', email,
'createdAt', created_at
) AS data
FROM tb_user;
GraphQL Type:
from fraiseql.types import ID
@fraiseql.type(sql_source="v_user", jsonb_column="data")
class User:
"""A user in the system.
Fields:
id: Unique user identifier
name: User's full name
email: User's email address
created_at: When the user was created
"""
id: ID
name: str
email: str
created_at: datetime
Benefits of View-Based Queries:
- ✅ Pre-formatted data - JSONB already in response format
- ✅ Rust pipeline - Zero Python serialization overhead
- ✅ Denormalized reads - Fast joins via Trinity Pattern (pk_*)
- ✅ Field selection - Only requested fields extracted
- ✅ Security - Views expose only public id (UUID), hide pk_*
Mutation Path (Write Operations)¶
Function-Based Mutations¶
flowchart LR
A[GraphQL Mutation] --> B[FraiseQL Schema]
B --> C[CALL fn_create_user]
C --> D[(PostgreSQL Function)]
D --> E{Validation}
E -->|Success| F[INSERT INTO tb_user]
E -->|Error| G[Return Error JSONB]
F --> H[Return Success JSONB]
G --> I[GraphQL Error Response]
H --> J[GraphQL Success Response]
style D fill:#fff3e0
style F fill:#ffe0b2
PostgreSQL Function Example:
-- Write-optimized function
CREATE OR REPLACE FUNCTION fn_create_user(
p_name TEXT,
p_email TEXT
) RETURNS JSONB AS $$
DECLARE
v_user_id UUID;
BEGIN
-- Validation
IF p_email IS NULL OR p_email = '' THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Email is required'
);
END IF;
-- Check uniqueness
IF EXISTS (SELECT 1 FROM tb_user WHERE email = p_email) THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Email already exists'
);
END IF;
-- Insert
INSERT INTO tb_user (name, email)
VALUES (p_name, p_email)
RETURNING id INTO v_user_id;
-- Success response
RETURN jsonb_build_object(
'success', true,
'userId', v_user_id
);
END;
$$ LANGUAGE plpgsql;
GraphQL Mutation:
@fraiseql.mutation
async def create_user(
info,
name: str,
email: str
) -> CreateUserResult:
"""Create a new user.
Args:
name: User's full name
email: User's email address
Returns:
CreateUserResult with success status and user ID or error message
"""
result = await info.context.db.execute(
"SELECT fn_create_user($1, $2)",
name,
email
)
return CreateUserResult.from_jsonb(result)
Benefits of Function-Based Mutations: - ✅ Business logic in database - Consistent validation rules - ✅ Transactional integrity - ACID guarantees - ✅ Security - Functions control what can be modified - ✅ Audit logging - All changes go through known entry points - ✅ Performance - No round-trips for validation queries
Schema Separation¶
flowchart TD
A[Database] --> B{Schema Type}
B -->|Read Schema| C[Views Layer]
B -->|Write Schema| D[Functions Layer]
B -->|Data Schema| E[Tables Layer]
C --> F[v_user<br/>v_post<br/>v_comment]
D --> G[fn_create_*<br/>fn_update_*<br/>fn_delete_*]
E --> H[tb_user<br/>tb_post<br/>tb_comment]
F --> I[GraphQL Queries]
G --> J[GraphQL Mutations]
H --> K[Internal Storage]
style C fill:#e8f5e9
style D fill:#fff3e0
style E fill:#e3f2fd
Three-Layer Pattern:
| Layer | Prefix | Purpose | Exposed To |
|---|---|---|---|
| Tables | tb_* |
Data storage | Internal only |
| Views | v_* |
Read-optimized queries | GraphQL queries |
| Functions | fn_* |
Write operations | GraphQL mutations |
Example Structure:
-- Layer 1: Data storage (internal)
CREATE TABLE tb_user (
pk_user INTEGER PRIMARY KEY,
id UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(),
identifier TEXT UNIQUE,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Layer 2: Read-optimized view (exposed to queries)
CREATE VIEW v_user AS
SELECT
id, -- Public UUID only
jsonb_build_object(
'id', id,
'name', name,
'email', email,
'identifier', identifier,
'createdAt', created_at
) AS data
FROM tb_user;
-- Layer 3: Write functions (exposed to mutations)
CREATE FUNCTION fn_create_user(...) RETURNS JSONB AS $$...$$;
CREATE FUNCTION fn_update_user(...) RETURNS JSONB AS $$...$$;
CREATE FUNCTION fn_delete_user(...) RETURNS JSONB AS $$...$$;
Query vs Mutation Characteristics¶
Queries (Read Path)¶
flowchart TD
A[Query Request] --> B[View Selection]
B --> C{Filters?}
C -->|Yes| D[WHERE Clause]
C -->|No| E[All Rows]
D --> F[Join Views]
E --> F
F --> G[JSONB Extraction]
G --> H[Rust Field Selection]
H --> I[Response]
style B fill:#e8f5e9
style H fill:#c8e6c9
Characteristics: - 📖 Read-only - No data modification - ⚡ Performance-focused - Denormalized views, JSONB pre-formatting - 🔄 Cacheable - Same query always returns same result - 🔒 Secure by default - Views expose only allowed fields - 🚀 Rust pipeline - Zero Python serialization
Example:
Mutations (Write Path)¶
flowchart TD
A[Mutation Request] --> B[Input Validation]
B --> C{Valid?}
C -->|No| D[Return Error]
C -->|Yes| E[Call PostgreSQL Function]
E --> F[Business Logic]
F --> G{Success?}
G -->|No| H[ROLLBACK]
G -->|Yes| I[COMMIT]
H --> J[Error Response]
I --> K[Success Response]
K --> L[Cache Invalidation]
style E fill:#fff3e0
style F fill:#ffe0b2
Characteristics: - ✏️ Write operations - Modify data - 🔒 Validated - Business rules enforced - 🔄 Transactional - ACID guarantees - 🚫 Not cacheable - Side effects - 📝 Audit trail - All changes logged
Example:
# Mutation (write)
mutation {
createUser(name: "John Doe", email: "john@example.com") {
success
userId
error
}
}
Integration with Trinity Pattern¶
CQRS and Trinity Pattern work together:
flowchart TD
A[Client Request] --> B{Operation Type?}
B -->|Query| C[View Layer]
B -->|Mutation| D[Function Layer]
C --> E[Expose: id UUID]
D --> F[Accept: id UUID]
E --> G[Hide: pk_* INTEGER]
F --> H[Resolve to: pk_* INTEGER]
G --> I[Security via Views]
H --> J[Performance via Functions]
I --> K[Fast Joins]
J --> K
style C fill:#e8f5e9
style D fill:#fff3e0
Views (Read):
- Expose only public id (UUID)
- Hide internal pk_* (INTEGER)
- Use fast pk_* joins internally
Functions (Write):
- Accept public id (UUID) as input
- Resolve to pk_* (INTEGER) internally
- Perform fast operations using pk_*
Example:
-- View exposes UUID
CREATE VIEW v_comment AS
SELECT
c.id, -- Public UUID
jsonb_build_object(
'id', c.id,
'userId', u.id, -- Public UUID relationship
'postId', p.id, -- Public UUID relationship
'content', c.content
) AS data
FROM tb_comment c
JOIN tb_user u ON u.pk_user = c.pk_user -- Fast integer join
JOIN tb_post p ON p.pk_post = c.pk_post -- Fast integer join
;
-- Function accepts UUID, uses pk_* internally
CREATE FUNCTION fn_create_comment(
p_user_id UUID, -- Public UUID
p_post_id UUID, -- Public UUID
p_content TEXT
) RETURNS JSONB AS $$
DECLARE
v_pk_user INTEGER;
v_pk_post INTEGER;
BEGIN
-- Resolve UUID → pk_* (once)
SELECT pk_user INTO v_pk_user FROM tb_user WHERE id = p_user_id;
SELECT pk_post INTO v_pk_post FROM tb_post WHERE id = p_post_id;
-- Use fast integer FK
INSERT INTO tb_comment (pk_user, pk_post, content)
VALUES (v_pk_user, v_pk_post, p_content);
RETURN jsonb_build_object('success', true);
END;
$$ LANGUAGE plpgsql;
Error Handling in CQRS¶
Query Errors (Read Path)¶
flowchart TD
A[Query] --> B{View Exists?}
B -->|No| C[Schema Error]
B -->|Yes| D{Valid Filters?}
D -->|No| E[Validation Error]
D -->|Yes| F[Execute Query]
F --> G{SQL Error?}
G -->|Yes| H[Database Error]
G -->|No| I[Success]
style C fill:#ffcccc
style E fill:#ffcccc
style H fill:#ffcccc
style I fill:#ccffcc
Query errors are rare: - Schema validation catches issues early - Views are read-only (no data integrity issues) - Filters validated before SQL execution
Mutation Errors (Write Path)¶
flowchart TD
A[Mutation] --> B{Input Valid?}
B -->|No| C[Return Validation Error JSONB]
B -->|Yes| D[Call Function]
D --> E{Business Rules OK?}
E -->|No| F[Return Business Error JSONB]
E -->|Yes| G{Database Constraints OK?}
G -->|No| H[Return Constraint Error JSONB]
G -->|Yes| I[Return Success JSONB]
style C fill:#ffe0b2
style F fill:#ffe0b2
style H fill:#ffe0b2
style I fill:#c8e6c9
Mutation error handling is explicit:
CREATE FUNCTION fn_create_user(
p_name TEXT,
p_email TEXT
) RETURNS JSONB AS $$
BEGIN
-- Validation error
IF p_email IS NULL THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Email is required',
'code', 'VALIDATION_ERROR'
);
END IF;
-- Business rule error
IF EXISTS (SELECT 1 FROM tb_user WHERE email = p_email) THEN
RETURN jsonb_build_object(
'success', false,
'error', 'Email already exists',
'code', 'DUPLICATE_EMAIL'
);
END IF;
-- Success
INSERT INTO tb_user (name, email) VALUES (p_name, p_email);
RETURN jsonb_build_object('success', true);
EXCEPTION
WHEN OTHERS THEN
-- Database error
RETURN jsonb_build_object(
'success', false,
'error', SQLERRM,
'code', 'DATABASE_ERROR'
);
END;
$$ LANGUAGE plpgsql;
Performance Benefits¶
Query Performance¶
| Aspect | Traditional ORM | FraiseQL CQRS |
|---|---|---|
| Serialization | Python objects → JSON | JSONB → Rust → JSON |
| Joins | ORM N+1 queries | Denormalized views |
| Field Selection | Full objects loaded | Only requested fields |
| Overhead | 25-60ms | 0.1-1ms |
Speedup: 25-60x faster than traditional ORM approaches
Mutation Performance¶
| Aspect | Traditional ORM | FraiseQL CQRS |
|---|---|---|
| Round-trips | Multiple queries | Single function call |
| Validation | Application layer | Database layer |
| Transactions | Manual management | Automatic in function |
| Audit | Application code | Database triggers |
Benefits: - ✅ Fewer network round-trips - ✅ Consistent validation rules - ✅ Guaranteed transactional integrity - ✅ Centralized audit logging
Best Practices¶
✅ DO¶
-
Use views for all queries
-
Use functions for all mutations
-
Return JSONB from functions
-
Expose only public
idin views -
Use
pk_*internally for joins
❌ DON'T¶
-
Don't query tables directly
-
Don't write INSERT/UPDATE in application code
-
Don't expose
pk_*in views -
Don't use UUID for foreign keys
-
Don't skip validation in functions
Naming Conventions¶
FraiseQL uses consistent naming for CQRS components:
| Component | Prefix | Example | Purpose |
|---|---|---|---|
| Tables | tb_ |
tb_user |
Data storage |
| Views | v_ |
v_user |
Read operations |
| Create Functions | fn_create_ |
fn_create_user |
Insert new records |
| Update Functions | fn_update_ |
fn_update_user |
Modify existing records |
| Delete Functions | fn_delete_ |
fn_delete_user |
Remove records |
| Custom Functions | fn_ |
fn_promote_user |
Business operations |
Consistency ensures: - Clear intent (table vs view vs function) - Easy to find related components - Obvious what each component does
Related Documentation¶
- Request Flow - How queries and mutations are executed
- Trinity Pattern - Three-identifier database design
- Type System - Type mapping across layers
- Database Patterns - More CQRS examples
Summary¶
FraiseQL's CQRS design provides:
✅ Clear Separation - Queries use views, mutations use functions ✅ Performance - Read-optimized views with Rust pipeline ✅ Security - Views expose only allowed fields, functions control writes ✅ Reliability - Business logic in database with ACID guarantees ✅ Maintainability - Consistent patterns and naming conventions ✅ Integration - Works seamlessly with Trinity Pattern for optimal performance
Golden Rule: Never query tables directly or write data without functions.