This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
giles fc14d8323a Add README documenting glue layer architecture
Covers models, services, handlers, event flows, and guidelines
for adding new cross-domain logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 19:18:01 +00:00

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_idchild_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), with depth
  • 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.pyregister_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.pyresolve_page_config() reads Calendar + MarketPlace
  • cart/bp/cart/api.py/summary reads CalendarEntry + Calendar + MarketPlace
  • cart/bp/cart/ → calendar_cart.py, page_cart.py read CalendarEntry/Calendar for display

Adding New Cross-Domain Logic

  1. Cross-domain write? → Add a function in glue/services/. Call it from the app code within the existing transaction.
  2. Eventually consistent? → Emit a domain event from the originating app. Add a handler in glue/handlers/.
  3. 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.
Description
Glue layer: container relationships and navigation
Readme 37 KiB
Languages
Python 100%