# Module Integration Schema
This document describes cross-module communication: external imports, exported types, event subscriptions, and the anti-corruption layer.
---
## Overview
Each module is a self-contained bounded context (see [ARCHITECTURE.md](./ARCHITECTURE.md)). Modules communicate through three channels only:
1. **External imports** — declared in `index.model.yaml`. Bring exported types from another module into the type namespace.
2. **Domain events** — emitted by one module, subscribed to by another via the `event-subscription` generator.
3. **ACL adapters** — generated by the `integration` target. Translate between internal entities and another module's external types.
> **Important**: Integration code is **automatically generated** from your model, hook, and workflow definitions. You write only the `external_imports` declaration and the workflow that handles the foreign event.
## Current `external_imports` Syntax
This is the syntax actually used by `bersihir`, `sapiens`, `bucket`, and `corpus`. The legacy `modules: depends_on: ...` syntax further down this document is kept for historical reference but new modules should use `external_imports`.
```yaml
# libs/modules/bersihir/schema/models/index.model.yaml
module: bersihir
version: 2
external_imports:
- module: sapiens
types: [User, Profile, Session]
# Now models in bersihir can reference sapiens.User in foreign keys:
```
In an entity model file:
```yaml
models:
- name: Customer
fields:
user_id:
type: uuid
attributes: ["@required", "@foreign_key(sapiens.User.id)"]
relations:
user:
type: sapiens.User?
attributes: ["@one", "@foreign_key(user_id)"]
```
The `sapiens.` prefix is what tells the validator to look in the `sapiens` module's exported types rather than the local module.
### Cross-Module Foreign Keys
Foreign keys to another module become real PostgreSQL FKs at the database level (assuming both modules share a database). At the Rust level, the generated entity holds a typed `UserId` from `sapiens` rather than re-declaring its own `User` type.
### Cross-Module Event Subscriptions
In a workflow file, trigger off an event from another module:
```yaml
# bersihir/schema/workflows/customer_registration.workflow.yaml
name: CustomerRegistration
trigger:
event: sapiens.UserRegisteredEvent # ← cross-module event
extract:
user_id: "event.user_id"
email: "event.email"
steps:
- name: create_customer_profile
type: action
action: create
entity: Customer
params:
user_id: "{{ context.user_id }}"
on_success:
next: complete
- name: complete
type: terminal
status: completed
```
The `event-subscription` generator emits a Rust subscriber that listens on the message bus for `sapiens::UserRegisteredEvent` and dispatches the workflow.
---
### Generation Source
| Exports (public DTOs, events) | `schema/models/*.model.yaml` | `export` |
| Integration Adapters | `schema/workflows/*.workflow.yaml` | `integration` |
| Event Subscriptions | `schema/workflows/*.workflow.yaml` | `event-subscription` |
### File Locations
```
# You write:
libs/modules/{module}/schema/models/user.model.yaml
libs/modules/{module}/schema/workflows/user_registration.workflow.yaml
# Generator creates:
libs/modules/{module}/src/
├── exports/
│ ├── user_export.rs # ← Generated from model (UserId, UserDto, etc.)
│ └── mod.rs
├── integration/
│ ├── user_adapter.rs # ← Generated from workflow
│ ├── integration_registry.rs
│ └── mod.rs
└── subscriptions/
├── user_event_handler.rs # ← Generated from workflow
└── mod.rs
```
---
## Module Dependencies
Reference other modules' exported types and traits.
```yaml
modules:
billing:
depends_on:
sapiens:
uses:
- UserLookup # Integration trait
- UserId # Value object
- UserRegistered # Event (for subscription)
- UserDeleted
```
### Dependency Rules
1. **ID only** - Store foreign IDs, not entities
2. **Integration traits** - Access other modules through defined interfaces
3. **Events** - React to other modules asynchronously
4. **No direct entity access** - Never import another module's entities
```
┌─────────────────────────────────────────────────────────────────────────┐
│ MODULE BOUNDARIES │
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ sapiens │ │ billing │ │
│ │ │ │ │ │
│ │ ┌───────────────┐ │ │ ┌───────────────┐ │ │
│ │ │ User │ │ │ │ Invoice │ │ │
│ │ │ Entity │ │ │ │ Entity │ │ │
│ │ └───────────────┘ │ │ │ │ │ │
│ │ │ │ │ │ user_id: ────────────┐ │
│ │ ▼ │ │ │ UserId │ │ │ │
│ │ ┌───────────────┐ │ │ └───────────────┘ │ │ │
│ │ │ exports: │◄─────────────────────────────────────┘ │
│ │ │ - UserId │ │ │ │ │
│ │ │ - UserLookup │ │ │ ┌───────────────┐ │ │
│ │ └───────────────┘ │ │ │ Integration │ │ │
│ │ │ │ │ UserLookup │──┘ │
│ │ │ │ │ (adapter) │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
│ Communication: ID only + Integration Traits + Events │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Exports
What a module exposes to other modules.
```yaml
modules:
sapiens:
exports:
# Type-safe ID wrapper
value_objects:
- UserId
- SessionId
# Read-only access traits
traits:
- UserLookup
- SessionValidator
# Events other modules can subscribe to
events:
- UserRegistered
- UserActivated
- UserDeactivated
- UserDeleted
- PasswordChanged
```
### Generated Exports
```rust
// sapiens/src/integration/exports.rs
/// Type-safe user ID for external modules
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct UserId(pub Uuid);
impl From<Uuid> for UserId {
fn from(id: Uuid) -> Self {
Self(id)
}
}
impl UserId {
pub fn as_uuid(&self) -> Uuid {
self.0
}
}
/// Minimal user info exposed to other modules
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInfo {
pub id: UserId,
pub email: String,
pub status: String, // String, not internal enum
}
/// What sapiens exposes for user lookups
#[async_trait]
pub trait UserLookup: Send + Sync {
async fn get_user_info(&self, user_id: UserId) -> Result<Option<UserInfo>, LookupError>;
async fn is_user_active(&self, user_id: UserId) -> Result<bool, LookupError>;
async fn get_user_email(&self, user_id: UserId) -> Result<Option<String>, LookupError>;
}
/// Session validation for other modules
#[async_trait]
pub trait SessionValidator: Send + Sync {
async fn validate_token(&self, token: &str) -> Result<Option<SessionInfo>, ValidationError>;
async fn get_user_for_session(&self, token: &str) -> Result<Option<UserId>, ValidationError>;
}
```
---
## Integration Traits (Anti-Corruption Layer)
Define how your module views external modules.
```yaml
modules:
billing:
integration:
# How billing sees users (anti-corruption layer)
UserLookup:
description: "Billing's view of users"
async: true
methods:
- name: get_billing_user
params:
- user_id: UserId
returns: Option<BillingUserInfo>
- name: is_user_active
params:
- user_id: UserId
returns: bool
- name: get_billing_tier
params:
- user_id: UserId
returns: BillingTier
# Billing-specific view of a user
value_objects:
BillingUserInfo:
description: "What billing needs to know about a user"
fields:
- id: UserId
- email: String
- billing_tier: BillingTier
- created_at: DateTime<Utc>
```
### Generated Adapter
```rust
// billing/src/integration/user_lookup_adapter.rs
use sapiens::exports::{UserId, UserLookup as SapiensUserLookup};
/// Billing's view of a user
#[derive(Debug, Clone)]
pub struct BillingUserInfo {
pub id: UserId,
pub email: String,
pub billing_tier: BillingTier,
pub created_at: DateTime<Utc>,
}
/// Adapter that transforms sapiens user into billing's view
pub struct UserLookupAdapter {
inner: Arc<dyn SapiensUserLookup>,
tier_service: Arc<dyn BillingTierService>,
}
impl UserLookupAdapter {
pub fn new(
inner: Arc<dyn SapiensUserLookup>,
tier_service: Arc<dyn BillingTierService>,
) -> Self {
Self { inner, tier_service }
}
pub async fn get_billing_user(&self, user_id: UserId) -> Option<BillingUserInfo> {
// <<< CUSTOM
let user_info = self.inner.get_user_info(user_id).await.ok()??;
let tier = self.tier_service
.get_tier_for_user(user_id)
.await
.unwrap_or(BillingTier::Free);
Some(BillingUserInfo {
id: user_info.id,
email: user_info.email,
billing_tier: tier,
created_at: DateTime::default(), // Get from user_info if available
})
// CUSTOM >>>
}
pub async fn is_user_active(&self, user_id: UserId) -> bool {
self.inner.is_user_active(user_id).await.unwrap_or(false)
}
pub async fn get_billing_tier(&self, user_id: UserId) -> BillingTier {
// <<< CUSTOM
self.tier_service
.get_tier_for_user(user_id)
.await
.unwrap_or(BillingTier::Free)
// CUSTOM >>>
}
}
```
---
## Event Subscriptions
React to events from other modules.
```yaml
modules:
billing:
subscribes_to:
sapiens:
UserRegistered:
handler: CreateBillingProfile
description: "Create billing profile for new user"
UserActivated:
handler: ActivateBillingProfile
description: "Enable billing for activated user"
UserDeleted:
handler: CleanupBillingData
description: "Cancel subscriptions, archive invoices"
notifications:
subscribes_to:
sapiens:
UserRegistered:
handler: SendWelcomeEmail
PasswordChanged:
handler: SendPasswordChangeAlert
LoginFailed:
handler: SendSecurityAlert
condition: "event.consecutive_failures >= 3"
billing:
InvoiceCreated:
handler: SendInvoiceEmail
InvoiceOverdue:
handler: SendOverdueReminder
SubscriptionCancelled:
handler: SendCancellationConfirmation
```
### Handler Configuration
```yaml
handlers:
CreateBillingProfile:
description: "Create billing profile when user registers"
event: sapiens.UserRegistered
dependencies:
- billing_profile_repo: BillingProfileRepository
- tier_service: BillingTierService
retry:
max_attempts: 3
backoff: exponential
initial_delay: 1s
max_delay: 1m
CleanupBillingData:
description: "Cleanup when user is deleted"
event: sapiens.UserDeleted
dependencies:
- billing_profile_repo: BillingProfileRepository
- subscription_service: SubscriptionService
- invoice_repo: InvoiceRepository
# Transaction for cleanup
transaction: required
retry:
max_attempts: 5
backoff: exponential
```
### Generated Code
```rust
// billing/src/application/handler/create_billing_profile.rs
use sapiens::events::UserRegistered;
pub struct CreateBillingProfileHandler {
billing_profile_repo: Arc<dyn BillingProfileRepository>,
tier_service: Arc<dyn BillingTierService>,
}
#[async_trait]
impl EventHandler<UserRegistered> for CreateBillingProfileHandler {
async fn handle(&self, event: UserRegistered) -> Result<(), HandlerError> {
// <<< CUSTOM
// Create billing profile with default tier
let profile = BillingProfile::new(
event.user_id,
BillingTier::Free,
);
self.billing_profile_repo.save(&profile).await?;
Ok(())
// CUSTOM >>>
}
fn retry_policy(&self) -> RetryPolicy {
RetryPolicy {
max_attempts: 3,
backoff: BackoffStrategy::Exponential,
initial_delay: Duration::from_secs(1),
max_delay: Duration::from_secs(60),
}
}
}
// billing/src/application/handler/cleanup_billing_data.rs
use sapiens::events::UserDeleted;
pub struct CleanupBillingDataHandler {
billing_profile_repo: Arc<dyn BillingProfileRepository>,
subscription_service: Arc<dyn SubscriptionService>,
invoice_repo: Arc<dyn InvoiceRepository>,
}
#[async_trait]
impl EventHandler<UserDeleted> for CleanupBillingDataHandler {
async fn handle(&self, event: UserDeleted) -> Result<(), HandlerError> {
// <<< CUSTOM
// Cancel all active subscriptions
self.subscription_service
.cancel_all_for_user(event.user_id, CancellationReason::UserDeleted)
.await?;
// Archive invoices
self.invoice_repo
.archive_for_user(event.user_id)
.await?;
// Soft delete billing profile
self.billing_profile_repo
.soft_delete(event.user_id)
.await?;
Ok(())
// CUSTOM >>>
}
fn requires_transaction(&self) -> bool {
true
}
}
```
### Handler Registration
```rust
// billing/src/infrastructure/event_handlers.rs
pub fn register_event_handlers(
event_bus: &mut EventBus,
container: &ServiceContainer,
) {
// Subscribe to sapiens events
event_bus.subscribe::<UserRegistered>(
container.resolve::<CreateBillingProfileHandler>()
);
event_bus.subscribe::<UserActivated>(
container.resolve::<ActivateBillingProfileHandler>()
);
event_bus.subscribe::<UserDeleted>(
container.resolve::<CleanupBillingDataHandler>()
);
}
```
---
## Handler Tracking
Track handler execution and retry failed handlers.
```yaml
handler_tracking:
enabled: true
retry_policy:
max_retries: 3
backoff: exponential
initial_delay: 1_second
max_delay: 1_minute
dead_letter:
enabled: true
table: dead_letter_events
retention: 30_days
monitoring:
metrics: true
alerts:
- condition: "failed_count > 10 in 1_hour"
severity: warning
- condition: "dead_letter_count > 5 in 1_day"
severity: critical
```
### Generated Table
```sql
CREATE TABLE event_handler_status (
id UUID PRIMARY KEY,
event_id BIGINT REFERENCES domain_events(event_id),
handler_name VARCHAR(255) NOT NULL,
module VARCHAR(100) NOT NULL,
status VARCHAR(20) NOT NULL, -- pending, processing, completed, failed, dead_letter
attempts INTEGER NOT NULL DEFAULT 0,
first_attempt TIMESTAMPTZ,
last_attempt TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
next_retry_at TIMESTAMPTZ,
last_error TEXT,
CONSTRAINT unique_handler_event UNIQUE (event_id, handler_name)
);
CREATE TABLE dead_letter_events (
id UUID PRIMARY KEY,
event_id BIGINT NOT NULL,
handler_name VARCHAR(255) NOT NULL,
module VARCHAR(100) NOT NULL,
event_type VARCHAR(255) NOT NULL,
payload JSONB NOT NULL,
error TEXT NOT NULL,
attempts INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
```
---
## Event Flow Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ EVENT FLOW │
│ │
│ ┌──────────────┐ │
│ │ sapiens │ │
│ │ │ │
│ │ UserService │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌────────┐ │ ┌─────────────┐ │
│ │ │ publish│──────► │ Event Bus │ │
│ │ └────────┘ │ └──────┬──────┘ │
│ │ │ │ │
│ │ UserRegistered │ │
│ └──────────────┘ │ │
│ ▼ │
│ ┌───────────────────┼───────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ billing │ │notifications │ │ analytics │ │
│ │ │ │ │ │ │ │
│ │ CreateBilling│ │ SendWelcome │ │ TrackSignup │ │
│ │ Profile │ │ Email │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## File Structure
```
libs/modules/billing/src/
├── integration/
│ ├── mod.rs
│ ├── exports/
│ │ ├── mod.rs
│ │ └── billing_events.rs
│ └── adapters/
│ ├── mod.rs
│ └── user_lookup_adapter.rs
│
├── application/
│ └── handler/
│ ├── mod.rs
│ ├── create_billing_profile.rs
│ ├── activate_billing_profile.rs
│ └── cleanup_billing_data.rs
│
└── infrastructure/
└── event_handlers.rs # Registration
```
---
## Related Documentation
- [ARCHITECTURE.md](./ARCHITECTURE.md) — generated DDD layers (domain events, event handlers, event store all live here)
- [RULE_FORMAT_WORKFLOWS.md](./RULE_FORMAT_WORKFLOWS.md) — workflow triggers and event subscriptions