Covers models, services, handlers, event flows, and guidelines for adding new cross-domain logic. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
5.2 KiB
Glue Layer
The glue layer sits between the four domain apps (blog, market, cart, events) and encapsulates all cross-domain logic — the code that would otherwise create tight coupling between services.
Each app includes glue/ as a git submodule. All apps share one PostgreSQL database, so glue services operate within the caller's existing DB transaction.
Architecture
blog ──┐
market ─┤
cart ───┤──→ glue (services, handlers, models)
events ─┘
Rule: Apps never import models or write data belonging to another domain directly. Instead they call a glue service or emit a domain event.
Models
ContainerRelation (models/container_relation.py)
Generic parent/child relationship between any two entities.
parent_type + parent_id→child_type + child_id- Soft-deletable (
deleted_at), ordered (sort_order) - Unique constraint on (parent_type, parent_id, child_type, child_id)
Used to attach calendars to pages, markets to pages, etc. without FK coupling.
MenuNode (models/menu_node.py)
Navigation tree nodes, scoped to a container (container_type + container_id).
- Self-referential (
parent_id), ordered (sort_order), withdepth - Stores
label,slug,href,icon,feature_image
Services
services/relationships.py — Container Relations
| Function | Description |
|---|---|
attach_child() |
Create or revive a ContainerRelation (upsert). Emits container.child_attached. |
get_children() |
Query children of a container, optionally filtered by child_type. |
detach_child() |
Soft-delete a ContainerRelation. Emits container.child_detached. |
services/order_lifecycle.py — Order ↔ Calendar Bridging
Cross-domain writes between cart and events domains. These run in the same transaction as the caller (not event-driven) because order creation and payment require immediate consistency.
| Function | Description |
|---|---|
claim_entries_for_order() |
Mark pending CalendarEntries as "ordered" and set order_id. Called from checkout. |
confirm_entries_for_order() |
Mark ordered CalendarEntries as "provisional". Called when payment confirms. |
get_entries_for_order() |
Return CalendarEntries for an order. Replaces the old Order.calendar_entries relationship. |
services/cart_adoption.py — Login Adoption
| Function | Description |
|---|---|
adopt_session_for_user() |
Adopt anonymous CartItems + CalendarEntries for a logged-in user. Soft-deletes any existing user cart/entries first. |
services/navigation.py — Navigation
| Function | Description |
|---|---|
get_navigation_tree() |
Return top-level MenuNodes ordered by sort_order. |
rebuild_navigation() |
Placeholder for syncing ContainerRelations → MenuNodes. |
Event Handlers
Handlers are registered at import time via shared.events.register_handler(). They're imported in setup.py → register_glue_handlers(), which each app calls at startup.
handlers/container_handlers.py
| Event | Handler |
|---|---|
container.child_attached |
Triggers rebuild_navigation() |
container.child_detached |
Triggers rebuild_navigation() |
handlers/login_handlers.py
| Event | Handler |
|---|---|
user.logged_in |
Calls adopt_session_for_user() to adopt anonymous cart/entries |
handlers/order_handlers.py
| Event | Handler |
|---|---|
order.created |
Logging (placeholder for future workflows) |
order.paid |
Logging (placeholder for future workflows) |
Event Flow Diagrams
Checkout
cart/checkout.py
├─ create order + order items (cart domain)
├─ glue: claim_entries_for_order() ← cross-domain write (same txn)
└─ emit: order.created ← observability
Payment Confirmation
cart/check_sumup_status.py
├─ update order.status = "paid" (cart domain)
├─ glue: confirm_entries_for_order() ← cross-domain write (same txn)
└─ emit: order.paid ← observability
Login Adoption
blog/auth/routes.py
└─ emit: user.logged_in ← event (in last_login_at txn)
↓ (EventProcessor)
glue/handlers/login_handlers.py
└─ glue: adopt_session_for_user() ← cross-domain write (handler txn)
Remaining Cross-Domain Reads (Pragmatic Exceptions)
These stay in the app code (not in glue) because they're read-only queries against the shared DB. Moving them to glue would be pure churn:
cart/bp/cart/services/checkout.py→resolve_page_config()reads Calendar + MarketPlacecart/bp/cart/api.py→/summaryreads CalendarEntry + Calendar + MarketPlacecart/bp/cart/→ calendar_cart.py, page_cart.py read CalendarEntry/Calendar for display
Adding New Cross-Domain Logic
- Cross-domain write? → Add a function in
glue/services/. Call it from the app code within the existing transaction. - Eventually consistent? → Emit a domain event from the originating app. Add a handler in
glue/handlers/. - Cross-domain read? → If it's a simple query against the shared DB, keep it in the app. Only move to glue if multiple apps need the same query.