Federation Reference
Complete federation API documentation.
This guide walks through building a complete e-commerce backend using FraiseQL’s federation capabilities to coordinate across three separate databases.
Client |FraiseQL API | +-- Primary DB (Orders, Customers, Shipping) +-- Inventory DB (Products, Stock, Reserves) +-- Payments DB (Ledger, Refunds, Billing)-- CustomersCREATE TABLE tb_customer ( pk_customer SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, identifier TEXT NOT NULL UNIQUE, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, phone TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
-- OrdersCREATE TABLE tb_order ( pk_order SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, identifier TEXT NOT NULL UNIQUE, fk_customer INTEGER NOT NULL REFERENCES tb_customer(pk_customer), total DECIMAL(10, 2) NOT NULL, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
-- Order ItemsCREATE TABLE tb_order_item ( pk_order_item SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, fk_order INTEGER NOT NULL REFERENCES tb_order(pk_order), product_id UUID NOT NULL, -- FK to inventory DB quantity INT NOT NULL, unit_price DECIMAL(10, 2) NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW());
-- Shipping InfoCREATE TABLE tb_shipping ( pk_shipping SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, fk_order INTEGER NOT NULL UNIQUE REFERENCES tb_order(pk_order), address TEXT NOT NULL, city TEXT NOT NULL, postal_code TEXT NOT NULL, country TEXT NOT NULL, tracking_number TEXT, status TEXT NOT NULL DEFAULT 'pending', estimated_delivery DATE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
-- Views for FraiseQL (always with JSONB data column)CREATE VIEW v_order ASSELECT o.id, o.identifier, jsonb_build_object( 'id', o.id::text, 'identifier', o.identifier, 'total', o.total, 'status', o.status, 'created_at', o.created_at, 'updated_at', o.updated_at ) AS dataFROM tb_order oWHERE o.deleted_at IS NULL;
CREATE VIEW v_customer ASSELECT c.id, c.identifier, jsonb_build_object( 'id', c.id::text, 'identifier', c.identifier, 'name', c.name, 'email', c.email, 'phone', c.phone, 'created_at', c.created_at ) AS dataFROM tb_customer cWHERE c.deleted_at IS NULL;-- PostgreSQL (separate instance)
CREATE TABLE tb_product ( pk_product SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, identifier TEXT NOT NULL UNIQUE, -- SKU slug sku TEXT UNIQUE NOT NULL, name TEXT NOT NULL, description TEXT, price DECIMAL(10, 2) NOT NULL, weight_grams INT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW());
CREATE TABLE tb_inventory ( pk_inventory SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, fk_product INTEGER NOT NULL REFERENCES tb_product(pk_product), warehouse TEXT NOT NULL, quantity INT NOT NULL DEFAULT 0, reserved INT NOT NULL DEFAULT 0, available INT GENERATED ALWAYS AS (quantity - reserved) STORED, updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(fk_product, warehouse));
CREATE TABLE tb_reservation ( pk_reservation SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, order_id UUID NOT NULL, -- FK to primary DB fk_product INTEGER NOT NULL REFERENCES tb_product(pk_product), quantity INT NOT NULL, status TEXT NOT NULL DEFAULT 'reserved', created_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ, released_at TIMESTAMPTZ);
CREATE VIEW v_product ASSELECT p.id, p.identifier, jsonb_build_object( 'id', p.id::text, 'sku', p.sku, 'name', p.name, 'price', p.price ) AS dataFROM tb_product p;
CREATE VIEW v_inventory_status ASSELECT p.id, p.identifier, jsonb_build_object( 'id', p.id::text, 'sku', p.sku, 'name', p.name, 'warehouse', i.warehouse, 'quantity', i.quantity, 'reserved', i.reserved, 'available', i.available ) AS dataFROM tb_product pLEFT JOIN tb_inventory i ON p.pk_product = i.fk_product;-- PostgreSQL (separate instance)
CREATE TABLE tb_payment_method ( pk_payment_method SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, customer_id UUID NOT NULL, -- FK to primary DB type TEXT NOT NULL, -- 'card', 'bank_transfer', 'wallet' token TEXT NOT NULL, -- Tokenized payment info is_default BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ DEFAULT NOW());
CREATE TABLE tb_transaction ( pk_transaction SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, order_id UUID NOT NULL UNIQUE, -- FK to primary DB customer_id UUID NOT NULL, amount DECIMAL(10, 2) NOT NULL, status TEXT NOT NULL DEFAULT 'pending', fk_payment_method INTEGER REFERENCES tb_payment_method(pk_payment_method), gateway TEXT, -- 'stripe', 'paypal', 'square' gateway_transaction_id TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), processed_at TIMESTAMPTZ, failed_at TIMESTAMPTZ);
CREATE TABLE tb_refund ( pk_refund SERIAL PRIMARY KEY, id UUID DEFAULT gen_random_uuid() NOT NULL UNIQUE, fk_transaction INTEGER NOT NULL REFERENCES tb_transaction(pk_transaction), order_id UUID NOT NULL, amount DECIMAL(10, 2) NOT NULL, reason TEXT, status TEXT NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT NOW(), processed_at TIMESTAMPTZ);
CREATE VIEW v_transaction ASSELECT t.id, jsonb_build_object( 'id', t.id::text, 'order_id', t.order_id::text, 'customer_id', t.customer_id::text, 'amount', t.amount, 'status', t.status, 'created_at', t.created_at ) AS dataFROM tb_transaction t;import fraiseqlfrom fraiseql.scalars import IDfrom decimal import Decimalfrom datetime import datetime
# ==================== PRIMARY DATABASE ====================
@fraiseql.typeclass Customer: id: ID name: str email: str phone: str | None created_at: datetime payment_methods: list['PaymentMethod'] # Federated from payments DB
@fraiseql.typeclass OrderItem: id: ID product_id: ID quantity: int unit_price: Decimal product: 'Product' # Federated from inventory DB
@fraiseql.typeclass Shipping: id: ID order_id: ID address: str city: str postal_code: str country: str tracking_number: str | None status: str estimated_delivery: str | None
@fraiseql.typeclass Order: id: ID total: Decimal status: str created_at: datetime updated_at: datetime items: list[OrderItem] # Local relation shipping: Shipping # Local relation customer: Customer # Federated from primary DB payment: 'Transaction' # Federated from payments DB reservations: list['Reservation'] # Federated from inventory DB
# ==================== INVENTORY DATABASE ====================
@fraiseql.typeclass Product: id: ID sku: str name: str price: Decimal
@fraiseql.typeclass InventoryStatus: product_id: ID sku: str name: str warehouse: str quantity: int reserved: int available: int
@fraiseql.typeclass Reservation: id: ID order_id: ID product_id: ID quantity: int status: str created_at: datetime expires_at: datetime | None released_at: datetime | None product: Product # Federated from inventory DB (same DB, different view)
# ==================== PAYMENTS DATABASE ====================
@fraiseql.typeclass PaymentMethod: id: ID customer_id: ID type: str is_default: bool
@fraiseql.typeclass Transaction: id: ID order_id: ID customer_id: ID amount: Decimal status: str gateway: str | None created_at: datetime processed_at: datetime | None
@fraiseql.typeclass Refund: id: ID transaction_id: ID order_id: ID amount: Decimal reason: str | None status: str created_at: datetime
# ==================== QUERIES ====================
@fraiseql.query(sql_source="v_order")def order(id: ID) -> Order: """Get order by ID with all federated relations.""" pass
@fraiseql.query(sql_source="v_order")def orders(customer_id: ID | None = None, limit: int = 20) -> list[Order]: """Get orders, optionally filtered by customer.""" pass
@fraiseql.query(sql_source="v_customer")def customer(id: ID) -> Customer: """Get customer with payment methods.""" pass
@fraiseql.query(sql_source="v_inventory_status")def inventory_status(product_id: ID | None = None) -> list[InventoryStatus]: """Get current inventory status across warehouses.""" pass
# ==================== MUTATIONS ====================
@fraiseql.mutation(sql_source="fn_create_order", operation="CREATE")def create_order( customer_id: ID, shipping_address: str, shipping_city: str, shipping_postal: str, shipping_country: str, payment_method_id: ID,) -> Order: """Create order. fn_create_order handles inventory reservation and payment via pg_notify.""" passThe create_order mutation coordinates writes across all three databases. In FraiseQL this is handled by a PostgreSQL function — the Python decorator is a compile-time declaration only. Cross-database coordination uses pg_notify events forwarded to NATS by FraiseQL observers.
CREATE OR REPLACE FUNCTION fn_create_order( p_customer_id UUID, p_shipping_address TEXT, p_shipping_city TEXT, p_shipping_postal TEXT, p_shipping_country TEXT, p_payment_method_id UUID)RETURNS SETOF v_orderLANGUAGE plpgsql AS $$DECLARE v_order tb_order;BEGIN -- Create order record INSERT INTO tb_order (fk_customer, total, status) SELECT pk_customer, 0, 'pending' FROM tb_customer WHERE id = p_customer_id RETURNING * INTO v_order;
-- Create shipping record INSERT INTO tb_shipping (fk_order, address, city, postal_code, country, status) VALUES (v_order.pk_order, p_shipping_address, p_shipping_city, p_shipping_postal, p_shipping_country, 'pending');
-- Notify inventory and payment services via pg_notify. -- FraiseQL observers forward these to NATS topics. PERFORM pg_notify( 'order_created', jsonb_build_object( 'order_id', v_order.id, 'customer_id', p_customer_id, 'payment_method_id', p_payment_method_id )::text );
RETURN QUERY SELECT * FROM v_order WHERE pk_order = v_order.pk_order;END;$$;Configure FraiseQL to forward pg_notify channels to NATS:
[federation]enabled = true
[[observers]]channel = "order_created"nats_subject = "fraiseql.order.created"
[[observers]]channel = "order_cancelled"nats_subject = "fraiseql.order.cancelled"Inventory reservation and payment processing are handled by separate services that subscribe to these NATS subjects. Each service updates its own database and emits completion/failure events, allowing independent rollback without a centralised saga framework.
query GetOrder { order(id: "550e8400-e29b-41d4-a716-446655440000") { id status total
customer { name email paymentMethods { type isDefault } }
items { quantity unitPrice product { sku name price } }
reservations { status product { name } }
payment { amount status processedAt } }}query CheckInventory { inventoryStatus(limit: 100) { productId sku name warehouse quantity reserved available }}Configure separate connection pools for each database:
[databases.primary]url = "${PRIMARY_DB}"pool_size = 50pool_timeout = 30000
[databases.inventory]url = "${INVENTORY_DB}"pool_size = 30pool_timeout = 30000
[databases.payments]url = "${PAYMENTS_DB}"pool_size = 20pool_timeout = 30000[federation]default_timeout = 5000 # 5 secondsbatch_size = 100
[federation.retry]max_attempts = 2backoff = "exponential"Federation Reference
Complete federation API documentation.
NATS Integration
Event coordination across services.
Error Handling
Saga failure recovery patterns.
Performance Guide
Query optimization for federated APIs.