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>
This commit is contained in:
122
README.md
Normal file
122
README.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 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`), 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.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 + 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.
|
||||||
Reference in New Issue
Block a user