Some checks failed
Go / build-and-release (push) Has been cancelled
- Add proper CORS headers for Blossom endpoints including X-SHA-256, X-Content-Length, X-Content-Type headers required by blossom-client-sdk - Add root-level Blossom routes (/upload, /media, /mirror, /report, /list/) for clients like Jumble that expect Blossom at root - Export BaseURLKey from pkg/blossom for use by app handlers - Make blossomRootHandler return URLs with /blossom prefix so blob downloads work via the registered /blossom/ route - Remove Access-Control-Allow-Credentials header (not needed for * origin) - Add Access-Control-Expose-Headers for X-Reason and other response headers Files modified: - app/blossom.go: Add blossomRootHandler, use exported BaseURLKey - app/server.go: Add CORS handling for blossom paths, register root routes - pkg/blossom/server.go: Fix CORS headers, export BaseURLKey - pkg/blossom/utils.go: Minor formatting - pkg/version/version: Bump to v0.36.12 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
767 lines
24 KiB
Markdown
767 lines
24 KiB
Markdown
# Domain-Driven Design Analysis: ORLY Relay
|
|
|
|
This document provides a comprehensive Domain-Driven Design (DDD) analysis of the ORLY Nostr relay codebase, evaluating its alignment with DDD principles and identifying opportunities for improvement.
|
|
|
|
---
|
|
|
|
## Key Recommendations Summary
|
|
|
|
| # | Recommendation | Impact | Effort |
|
|
|---|----------------|--------|--------|
|
|
| 1 | [Formalize Domain Events](#1-formalize-domain-events) | High | Medium |
|
|
| 2 | [Strengthen Aggregate Boundaries](#2-strengthen-aggregate-boundaries) | High | Medium |
|
|
| 3 | [Extract Application Services](#3-extract-application-services) | Medium | High |
|
|
| 4 | [Establish Ubiquitous Language Glossary](#4-establish-ubiquitous-language-glossary) | Medium | Low |
|
|
| 5 | [Add Domain-Specific Error Types](#5-add-domain-specific-error-types) | Medium | Low |
|
|
| 6 | [Enforce Value Object Immutability](#6-enforce-value-object-immutability) | Low | Low |
|
|
| 7 | [Document Context Map](#7-document-context-map) | Medium | Low |
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Executive Summary](#executive-summary)
|
|
2. [Strategic Design Analysis](#strategic-design-analysis)
|
|
- [Bounded Contexts](#bounded-contexts)
|
|
- [Context Map](#context-map)
|
|
- [Subdomain Classification](#subdomain-classification)
|
|
3. [Tactical Design Analysis](#tactical-design-analysis)
|
|
- [Entities](#entities)
|
|
- [Value Objects](#value-objects)
|
|
- [Aggregates](#aggregates)
|
|
- [Repositories](#repositories)
|
|
- [Domain Services](#domain-services)
|
|
- [Domain Events](#domain-events)
|
|
4. [Anti-Patterns Identified](#anti-patterns-identified)
|
|
5. [Detailed Recommendations](#detailed-recommendations)
|
|
6. [Implementation Checklist](#implementation-checklist)
|
|
7. [Appendix: File References](#appendix-file-references)
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
ORLY demonstrates **mature DDD adoption** for a system of its complexity. The codebase exhibits clear bounded context separation, proper repository patterns with multiple backend implementations, and well-designed interface segregation that prevents circular dependencies.
|
|
|
|
**Strengths:**
|
|
- Clear separation between `app/` (application layer) and `pkg/` (domain/infrastructure)
|
|
- Repository pattern with three interchangeable backends (Badger, Neo4j, WasmDB)
|
|
- Interface-based ACL system with pluggable implementations
|
|
- Per-connection aggregate isolation in `Listener`
|
|
- Strong use of Go interfaces for dependency inversion
|
|
|
|
**Areas for Improvement:**
|
|
- Domain events are implicit rather than explicit types
|
|
- Some aggregates expose mutable state via public fields
|
|
- Handler methods mix application orchestration with domain logic
|
|
- Ubiquitous language is partially documented
|
|
|
|
**Overall DDD Maturity Score: 7/10**
|
|
|
|
---
|
|
|
|
## Strategic Design Analysis
|
|
|
|
### Bounded Contexts
|
|
|
|
ORLY organizes code into distinct bounded contexts, each with its own model and language:
|
|
|
|
#### 1. Event Storage Context (`pkg/database/`)
|
|
- **Responsibility:** Persistent storage of Nostr events
|
|
- **Key Abstractions:** `Database` interface, `Subscription`, `Payment`
|
|
- **Implementations:** Badger (embedded), Neo4j (graph), WasmDB (browser)
|
|
- **File:** `pkg/database/interface.go:17-109`
|
|
|
|
#### 2. Access Control Context (`pkg/acl/`)
|
|
- **Responsibility:** Authorization decisions for read/write operations
|
|
- **Key Abstractions:** `I` interface, `Registry`, access levels
|
|
- **Implementations:** `None`, `Follows`, `Managed`
|
|
- **Files:** `pkg/acl/acl.go`, `pkg/interfaces/acl/acl.go:21-34`
|
|
|
|
#### 3. Event Policy Context (`pkg/policy/`)
|
|
- **Responsibility:** Event filtering, validation, and rate limiting rules
|
|
- **Key Abstractions:** `Rule`, `Kinds`, `PolicyManager`
|
|
- **Invariants:** Whitelist/blacklist precedence, size limits, tag requirements
|
|
- **File:** `pkg/policy/policy.go:58-180`
|
|
|
|
#### 4. Connection Management Context (`app/`)
|
|
- **Responsibility:** WebSocket lifecycle, message routing, authentication
|
|
- **Key Abstractions:** `Listener`, `Server`, message handlers
|
|
- **File:** `app/listener.go:24-52`
|
|
|
|
#### 5. Protocol Extensions Context (`pkg/protocol/`)
|
|
- **Responsibility:** NIP implementations beyond core protocol
|
|
- **Subcontexts:**
|
|
- NIP-43 Membership (`pkg/protocol/nip43/`)
|
|
- Graph queries (`pkg/protocol/graph/`)
|
|
- NWC payments (`pkg/protocol/nwc/`)
|
|
- Sync/replication (`pkg/sync/`)
|
|
|
|
#### 6. Rate Limiting Context (`pkg/ratelimit/`)
|
|
- **Responsibility:** Adaptive throttling based on system load
|
|
- **Key Abstractions:** `Limiter`, `Monitor`, PID controller
|
|
- **Integration:** Memory pressure from database backends
|
|
|
|
### Context Map
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ Connection Management (app/) │
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
│ │ Server │───▶│ Listener │───▶│ Handlers │ │
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
|
└────────┬────────────────────┬────────────────────┬──────────────────────┘
|
|
│ │ │
|
|
│ [Conformist] │ [Customer-Supplier]│ [Customer-Supplier]
|
|
▼ ▼ ▼
|
|
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
|
|
│ Access Control│ │ Event Storage │ │ Event Policy │
|
|
│ (pkg/acl/) │ │ (pkg/database/)│ │ (pkg/policy/) │
|
|
│ │ │ │ │ │
|
|
│ Registry ◀────┼───┼────Conformist──┼───┼─▶ Manager │
|
|
└────────────────┘ └────────────────┘ └────────────────┘
|
|
│ │ │
|
|
│ │ [Shared Kernel] │
|
|
│ ▼ │
|
|
│ ┌────────────────┐ │
|
|
│ │ Event Entity │ │
|
|
│ │(git.mleku.dev/ │◀───────────┘
|
|
│ │ mleku/nostr) │
|
|
│ └────────────────┘
|
|
│ │
|
|
│ [Anti-Corruption] │ [Customer-Supplier]
|
|
▼ ▼
|
|
┌────────────────┐ ┌────────────────┐
|
|
│ Rate Limiting │ │ Protocol │
|
|
│ (pkg/ratelimit)│ │ Extensions │
|
|
│ │ │ (pkg/protocol/)│
|
|
└────────────────┘ └────────────────┘
|
|
```
|
|
|
|
**Integration Patterns Identified:**
|
|
|
|
| Upstream | Downstream | Pattern | Notes |
|
|
|----------|------------|---------|-------|
|
|
| nostr library | All contexts | Shared Kernel | Event, Filter, Tag types |
|
|
| Database | ACL, Policy | Customer-Supplier | Query for follow lists, permissions |
|
|
| Policy | Handlers | Conformist | Handlers respect policy decisions |
|
|
| ACL | Handlers | Conformist | Handlers respect access levels |
|
|
| Rate Limit | Database | Anti-Corruption | Load monitor abstraction |
|
|
|
|
### Subdomain Classification
|
|
|
|
| Subdomain | Type | Justification |
|
|
|-----------|------|---------------|
|
|
| Event Storage | **Core** | Central to relay's value proposition |
|
|
| Access Control | **Core** | Key differentiator (WoT, follows-based) |
|
|
| Event Policy | **Core** | Enables complex filtering rules |
|
|
| Connection Management | **Supporting** | Standard WebSocket infrastructure |
|
|
| Rate Limiting | **Supporting** | Operational concern, not domain-specific |
|
|
| NIP-43 Membership | **Core** | Unique invite-based access model |
|
|
| Sync/Replication | **Supporting** | Infrastructure for federation |
|
|
|
|
---
|
|
|
|
## Tactical Design Analysis
|
|
|
|
### Entities
|
|
|
|
Entities are objects with identity that persists across state changes.
|
|
|
|
#### Listener (Connection Entity)
|
|
```go
|
|
// app/listener.go:24-52
|
|
type Listener struct {
|
|
conn *websocket.Conn // Identity: connection handle
|
|
challenge atomicutils.Bytes // Auth challenge state
|
|
authedPubkey atomicutils.Bytes // Authenticated identity
|
|
subscriptions map[string]context.CancelFunc
|
|
// ... more fields
|
|
}
|
|
```
|
|
- **Identity:** WebSocket connection pointer
|
|
- **Lifecycle:** Created on connect, destroyed on disconnect
|
|
- **Invariants:** Only one authenticated pubkey per connection
|
|
|
|
#### InviteCode (NIP-43 Entity)
|
|
```go
|
|
// pkg/protocol/nip43/types.go:26-31
|
|
type InviteCode struct {
|
|
Code string // Identity: unique code
|
|
ExpiresAt time.Time
|
|
UsedBy []byte // Tracks consumption
|
|
CreatedAt time.Time
|
|
}
|
|
```
|
|
- **Identity:** Unique code string
|
|
- **Lifecycle:** Created → Valid → Used/Expired
|
|
- **Invariants:** Cannot be reused once consumed
|
|
|
|
#### Subscription (Payment Entity)
|
|
```go
|
|
// pkg/database/interface.go (implied by methods)
|
|
// GetSubscription, ExtendSubscription, RecordPayment
|
|
```
|
|
- **Identity:** Pubkey
|
|
- **Lifecycle:** Trial → Active → Expired
|
|
- **Invariants:** Can only extend if not expired
|
|
|
|
### Value Objects
|
|
|
|
Value objects are immutable and defined by their attributes, not identity.
|
|
|
|
#### IdPkTs (Event Reference)
|
|
```go
|
|
// pkg/interfaces/store/store_interface.go:63-68
|
|
type IdPkTs struct {
|
|
Id []byte // Event ID
|
|
Pub []byte // Pubkey
|
|
Ts int64 // Timestamp
|
|
Ser uint64 // Serial number
|
|
}
|
|
```
|
|
- **Equality:** By all fields
|
|
- **Issue:** Should be immutable but uses mutable slices
|
|
|
|
#### Kinds (Policy Specification)
|
|
```go
|
|
// pkg/policy/policy.go:55-63
|
|
type Kinds struct {
|
|
Whitelist []int `json:"whitelist,omitempty"`
|
|
Blacklist []int `json:"blacklist,omitempty"`
|
|
}
|
|
```
|
|
- **Equality:** By whitelist/blacklist contents
|
|
- **Semantics:** Whitelist takes precedence over blacklist
|
|
|
|
#### Rule (Policy Rule)
|
|
```go
|
|
// pkg/policy/policy.go:75-180
|
|
type Rule struct {
|
|
Description string
|
|
WriteAllow []string
|
|
WriteDeny []string
|
|
MaxExpiry *int64
|
|
SizeLimit *int64
|
|
// ... 25+ fields
|
|
}
|
|
```
|
|
- **Issue:** Very large, could benefit from decomposition
|
|
- **Binary caches:** Performance optimization for hex→binary conversion
|
|
|
|
#### WriteRequest (Message Value)
|
|
```go
|
|
// pkg/protocol/publish/types.go (implied)
|
|
type WriteRequest struct {
|
|
Data []byte
|
|
MsgType int
|
|
IsControl bool
|
|
Deadline time.Time
|
|
}
|
|
```
|
|
|
|
### Aggregates
|
|
|
|
Aggregates are clusters of entities/value objects with consistency boundaries.
|
|
|
|
#### Listener Aggregate
|
|
- **Root:** `Listener`
|
|
- **Members:** Subscriptions map, auth state, write channel
|
|
- **Boundary:** Per-connection isolation
|
|
- **Invariants:**
|
|
- Subscriptions must exist before receiving matching events
|
|
- AUTH must complete before other messages check authentication
|
|
- Message processing is serialized within connection
|
|
|
|
```go
|
|
// app/listener.go:226-238 - Aggregate consistency enforcement
|
|
l.authProcessing.Lock()
|
|
if isAuthMessage {
|
|
// Process AUTH synchronously while holding lock
|
|
l.HandleMessage(req.data, req.remote)
|
|
l.authProcessing.Unlock()
|
|
} else {
|
|
l.authProcessing.Unlock()
|
|
// Process concurrently
|
|
}
|
|
```
|
|
|
|
#### Event Aggregate (External)
|
|
- **Root:** `event.E` (from nostr library)
|
|
- **Members:** Tags, signature, content
|
|
- **Invariants:**
|
|
- ID must match computed hash
|
|
- Signature must be valid
|
|
- Timestamp must be within bounds
|
|
- **Validation:** `app/handle-event.go:348-390`
|
|
|
|
#### InviteCode Aggregate
|
|
- **Root:** `InviteCode`
|
|
- **Members:** Code, expiry, usage tracking
|
|
- **Invariants:**
|
|
- Code uniqueness
|
|
- Single-use enforcement
|
|
- Expiry validation
|
|
|
|
### Repositories
|
|
|
|
The Repository pattern abstracts persistence for aggregate roots.
|
|
|
|
#### Database Interface (Primary Repository)
|
|
```go
|
|
// pkg/database/interface.go:17-109
|
|
type Database interface {
|
|
// Event persistence
|
|
SaveEvent(c context.Context, ev *event.E) (exists bool, err error)
|
|
QueryEvents(c context.Context, f *filter.F) (evs event.S, err error)
|
|
DeleteEvent(c context.Context, eid []byte) error
|
|
|
|
// Subscription management
|
|
GetSubscription(pubkey []byte) (*Subscription, error)
|
|
ExtendSubscription(pubkey []byte, days int) error
|
|
|
|
// NIP-43 membership
|
|
AddNIP43Member(pubkey []byte, inviteCode string) error
|
|
IsNIP43Member(pubkey []byte) (isMember bool, err error)
|
|
|
|
// ... 50+ methods
|
|
}
|
|
```
|
|
|
|
**Repository Implementations:**
|
|
1. **Badger** (`pkg/database/database.go`): Embedded key-value store
|
|
2. **Neo4j** (`pkg/neo4j/`): Graph database for social queries
|
|
3. **WasmDB** (`pkg/wasmdb/`): Browser IndexedDB for WASM builds
|
|
|
|
**Interface Segregation:**
|
|
```go
|
|
// pkg/interfaces/store/store_interface.go:21-37
|
|
type I interface {
|
|
Pather
|
|
io.Closer
|
|
Wiper
|
|
Querier // QueryForIds
|
|
Querent // QueryEvents
|
|
Deleter // DeleteEvent
|
|
Saver // SaveEvent
|
|
Importer
|
|
Exporter
|
|
Syncer
|
|
// ...
|
|
}
|
|
```
|
|
|
|
### Domain Services
|
|
|
|
Domain services encapsulate logic that doesn't belong to any single entity.
|
|
|
|
#### ACL Registry (Access Decision Service)
|
|
```go
|
|
// pkg/acl/acl.go:40-48
|
|
func (s *S) GetAccessLevel(pub []byte, address string) (level string) {
|
|
for _, i := range s.ACL {
|
|
if i.Type() == s.Active.Load() {
|
|
level = i.GetAccessLevel(pub, address)
|
|
break
|
|
}
|
|
}
|
|
return
|
|
}
|
|
```
|
|
- Delegates to active ACL implementation
|
|
- Stateless decision based on pubkey and IP
|
|
|
|
#### Policy Manager (Event Validation Service)
|
|
```go
|
|
// pkg/policy/policy.go (P type, CheckPolicy method)
|
|
// Evaluates rule chains, scripts, whitelist/blacklist logic
|
|
```
|
|
- Complex rule evaluation logic
|
|
- Script execution for custom validation
|
|
|
|
#### InviteManager (Invite Lifecycle Service)
|
|
```go
|
|
// pkg/protocol/nip43/types.go:34-109
|
|
type InviteManager struct {
|
|
codes map[string]*InviteCode
|
|
expiry time.Duration
|
|
}
|
|
func (im *InviteManager) GenerateCode() (code string, err error)
|
|
func (im *InviteManager) ValidateAndConsume(code string, pubkey []byte) (bool, string)
|
|
```
|
|
- Manages invite code lifecycle
|
|
- Thread-safe with mutex protection
|
|
|
|
### Domain Events
|
|
|
|
**Current State:** Domain events are implicit in message flow, not explicit types.
|
|
|
|
**Implicit Events Identified:**
|
|
|
|
| Event | Trigger | Effect |
|
|
|-------|---------|--------|
|
|
| EventPublished | `SaveEvent()` success | `publishers.Deliver()` |
|
|
| EventDeleted | Kind 5 processing | Cascade delete targets |
|
|
| UserAuthenticated | AUTH envelope accepted | `authedPubkey` set |
|
|
| SubscriptionCreated | REQ envelope | Query + stream setup |
|
|
| MembershipAdded | NIP-43 join request | ACL update |
|
|
| PolicyUpdated | Policy config event | `messagePauseMutex.Lock()` |
|
|
|
|
---
|
|
|
|
## Anti-Patterns Identified
|
|
|
|
### 1. Large Handler Methods (Partial Anemic Domain Model)
|
|
|
|
**Location:** `app/handle-event.go:183-783` (600+ lines)
|
|
|
|
**Issue:** The `HandleEvent` method contains:
|
|
- Input validation
|
|
- Policy checking
|
|
- ACL verification
|
|
- Signature verification
|
|
- Persistence
|
|
- Event delivery
|
|
- Special case handling (delete, ephemeral, NIP-43)
|
|
|
|
**Impact:** Difficult to test, maintain, and understand. Business rules are embedded in orchestration code.
|
|
|
|
### 2. Mutable Value Object Fields
|
|
|
|
**Location:** `pkg/interfaces/store/store_interface.go:63-68`
|
|
|
|
```go
|
|
type IdPkTs struct {
|
|
Id []byte // Mutable slice
|
|
Pub []byte // Mutable slice
|
|
Ts int64
|
|
Ser uint64
|
|
}
|
|
```
|
|
|
|
**Impact:** Value objects should be immutable. Callers could accidentally mutate shared state.
|
|
|
|
### 3. Global Singleton Registry
|
|
|
|
**Location:** `pkg/acl/acl.go:10`
|
|
|
|
```go
|
|
var Registry = &S{}
|
|
```
|
|
|
|
**Impact:** Global state makes testing difficult and hides dependencies. Should be injected.
|
|
|
|
### 4. Missing Domain Events
|
|
|
|
**Impact:** Side effects are coupled to primary operations. Adding new behaviors (logging, analytics, notifications) requires modifying core handlers.
|
|
|
|
### 5. Oversized Rule Value Object
|
|
|
|
**Location:** `pkg/policy/policy.go:75-180`
|
|
|
|
The `Rule` struct has 25+ fields, suggesting it might benefit from decomposition into smaller, focused value objects:
|
|
- `AccessRule` (allow/deny lists)
|
|
- `SizeRule` (limits)
|
|
- `TimeRule` (expiry, age)
|
|
- `ValidationRule` (tags, regex)
|
|
|
|
---
|
|
|
|
## Detailed Recommendations
|
|
|
|
### 1. Formalize Domain Events
|
|
|
|
**Problem:** Side effects are tightly coupled to primary operations.
|
|
|
|
**Solution:** Create explicit domain event types and a simple event dispatcher.
|
|
|
|
```go
|
|
// pkg/domain/events/events.go
|
|
package events
|
|
|
|
type DomainEvent interface {
|
|
OccurredAt() time.Time
|
|
AggregateID() []byte
|
|
}
|
|
|
|
type EventPublished struct {
|
|
EventID []byte
|
|
Pubkey []byte
|
|
Kind int
|
|
Timestamp time.Time
|
|
}
|
|
|
|
type MembershipGranted struct {
|
|
Pubkey []byte
|
|
InviteCode string
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// Simple dispatcher
|
|
type Dispatcher struct {
|
|
handlers map[reflect.Type][]func(DomainEvent)
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Decoupled side effects
|
|
- Easier testing
|
|
- Audit trail capability
|
|
- Foundation for event sourcing if needed
|
|
|
|
**Files to Modify:**
|
|
- Create `pkg/domain/events/`
|
|
- Update `app/handle-event.go` to emit events
|
|
- Update `app/handle-nip43.go` for membership events
|
|
|
|
### 2. Strengthen Aggregate Boundaries
|
|
|
|
**Problem:** Aggregate internals are exposed via public fields.
|
|
|
|
**Solution:** Use unexported fields with behavior methods.
|
|
|
|
```go
|
|
// Before (current)
|
|
type Listener struct {
|
|
authedPubkey atomicutils.Bytes // Accessible from outside
|
|
}
|
|
|
|
// After (recommended)
|
|
type Listener struct {
|
|
authedPubkey atomicutils.Bytes // Keep as is (already using atomic wrapper)
|
|
}
|
|
|
|
// Add behavior methods
|
|
func (l *Listener) IsAuthenticated() bool {
|
|
return len(l.authedPubkey.Load()) > 0
|
|
}
|
|
|
|
func (l *Listener) AuthenticatedPubkey() []byte {
|
|
return l.authedPubkey.Load()
|
|
}
|
|
|
|
func (l *Listener) Authenticate(pubkey []byte) error {
|
|
if l.IsAuthenticated() {
|
|
return ErrAlreadyAuthenticated
|
|
}
|
|
l.authedPubkey.Store(pubkey)
|
|
return nil
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Enforces invariants
|
|
- Clear API surface
|
|
- Easier refactoring
|
|
|
|
### 3. Extract Application Services
|
|
|
|
**Problem:** Handler methods contain mixed concerns.
|
|
|
|
**Solution:** Extract domain logic into focused application services.
|
|
|
|
```go
|
|
// pkg/application/event_service.go
|
|
type EventService struct {
|
|
db database.Database
|
|
policyMgr *policy.P
|
|
aclRegistry *acl.S
|
|
eventPublisher EventPublisher
|
|
}
|
|
|
|
func (s *EventService) ProcessIncomingEvent(ctx context.Context, ev *event.E, authedPubkey []byte) (*EventResult, error) {
|
|
// 1. Validate event structure
|
|
if err := s.validateEventStructure(ev); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 2. Check policy
|
|
if !s.policyMgr.IsAllowed("write", ev, authedPubkey) {
|
|
return &EventResult{Blocked: true, Reason: "policy"}, nil
|
|
}
|
|
|
|
// 3. Check ACL
|
|
if !s.aclRegistry.CanWrite(authedPubkey) {
|
|
return &EventResult{Blocked: true, Reason: "acl"}, nil
|
|
}
|
|
|
|
// 4. Persist
|
|
exists, err := s.db.SaveEvent(ctx, ev)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// 5. Publish domain event
|
|
s.eventPublisher.Publish(events.EventPublished{...})
|
|
|
|
return &EventResult{Saved: !exists}, nil
|
|
}
|
|
```
|
|
|
|
**Benefits:**
|
|
- Testable business logic
|
|
- Handlers become thin orchestrators
|
|
- Reusable across different entry points (WebSocket, HTTP API, CLI)
|
|
|
|
### 4. Establish Ubiquitous Language Glossary
|
|
|
|
**Problem:** Terminology is inconsistent across the codebase.
|
|
|
|
**Current Inconsistencies:**
|
|
- "subscription" (payment) vs "subscription" (REQ filter)
|
|
- "monitor" (rate limit) vs "spider" (sync)
|
|
- "pub" vs "pubkey" vs "author"
|
|
|
|
**Solution:** Add a `GLOSSARY.md` and enforce terms in code reviews.
|
|
|
|
```markdown
|
|
# ORLY Ubiquitous Language
|
|
|
|
## Core Domain Terms
|
|
|
|
| Term | Definition | Code Symbol |
|
|
|------|------------|-------------|
|
|
| Event | A signed Nostr message | `event.E` |
|
|
| Relay | This server | `Server` |
|
|
| Connection | WebSocket session | `Listener` |
|
|
| Filter | Query criteria for events | `filter.F` |
|
|
| **Event Subscription** | Active filter receiving events | `subscriptions map` |
|
|
| **Payment Subscription** | Paid access tier | `database.Subscription` |
|
|
| Access Level | Permission tier (none/read/write/admin/owner) | `acl.Level` |
|
|
| Policy | Event validation rules | `policy.Rule` |
|
|
```
|
|
|
|
### 5. Add Domain-Specific Error Types
|
|
|
|
**Problem:** Errors are strings or generic types, making error handling imprecise.
|
|
|
|
**Solution:** Create typed domain errors.
|
|
|
|
```go
|
|
// pkg/domain/errors/errors.go
|
|
package errors
|
|
|
|
type DomainError struct {
|
|
Code string
|
|
Message string
|
|
Cause error
|
|
}
|
|
|
|
var (
|
|
ErrEventInvalid = &DomainError{Code: "EVENT_INVALID"}
|
|
ErrEventBlocked = &DomainError{Code: "EVENT_BLOCKED"}
|
|
ErrAuthRequired = &DomainError{Code: "AUTH_REQUIRED"}
|
|
ErrQuotaExceeded = &DomainError{Code: "QUOTA_EXCEEDED"}
|
|
ErrInviteCodeInvalid = &DomainError{Code: "INVITE_INVALID"}
|
|
ErrInviteCodeExpired = &DomainError{Code: "INVITE_EXPIRED"}
|
|
ErrInviteCodeUsed = &DomainError{Code: "INVITE_USED"}
|
|
)
|
|
```
|
|
|
|
**Benefits:**
|
|
- Precise error handling in handlers
|
|
- Better error messages to clients
|
|
- Easier testing
|
|
|
|
### 6. Enforce Value Object Immutability
|
|
|
|
**Problem:** Value objects use mutable slices.
|
|
|
|
**Solution:** Return copies from accessors.
|
|
|
|
```go
|
|
// pkg/interfaces/store/store_interface.go
|
|
type IdPkTs struct {
|
|
id []byte // unexported
|
|
pub []byte // unexported
|
|
ts int64
|
|
ser uint64
|
|
}
|
|
|
|
func NewIdPkTs(id, pub []byte, ts int64, ser uint64) *IdPkTs {
|
|
return &IdPkTs{
|
|
id: append([]byte(nil), id...), // Copy
|
|
pub: append([]byte(nil), pub...), // Copy
|
|
ts: ts,
|
|
ser: ser,
|
|
}
|
|
}
|
|
|
|
func (i *IdPkTs) ID() []byte { return append([]byte(nil), i.id...) }
|
|
func (i *IdPkTs) Pub() []byte { return append([]byte(nil), i.pub...) }
|
|
func (i *IdPkTs) Ts() int64 { return i.ts }
|
|
func (i *IdPkTs) Ser() uint64 { return i.ser }
|
|
```
|
|
|
|
### 7. Document Context Map
|
|
|
|
**Problem:** Context relationships are implicit.
|
|
|
|
**Solution:** Add a `CONTEXT_MAP.md` documenting boundaries and integration patterns.
|
|
|
|
The diagram in the [Context Map](#context-map) section above should be maintained as living documentation.
|
|
|
|
---
|
|
|
|
## Implementation Checklist
|
|
|
|
### Currently Satisfied
|
|
|
|
- [x] Bounded contexts identified with clear boundaries
|
|
- [x] Repositories abstract persistence for aggregate roots
|
|
- [x] Multiple repository implementations (Badger/Neo4j/WasmDB)
|
|
- [x] Interface segregation prevents circular dependencies
|
|
- [x] Configuration centralized (`app/config/config.go`)
|
|
- [x] Per-connection aggregate isolation
|
|
- [x] Access control as pluggable strategy pattern
|
|
|
|
### Needs Attention
|
|
|
|
- [ ] Ubiquitous language documented and used consistently
|
|
- [ ] Context map documenting integration patterns
|
|
- [ ] Domain events capture important state changes
|
|
- [ ] Entities have behavior, not just data
|
|
- [ ] Value objects are fully immutable
|
|
- [ ] No business logic in application services (orchestration only)
|
|
- [ ] No infrastructure concerns in domain layer
|
|
|
|
---
|
|
|
|
## Appendix: File References
|
|
|
|
### Core Domain Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `pkg/database/interface.go` | Repository interface (50+ methods) |
|
|
| `pkg/interfaces/acl/acl.go` | ACL interface definition |
|
|
| `pkg/interfaces/store/store_interface.go` | Store sub-interfaces |
|
|
| `pkg/policy/policy.go` | Policy rules and evaluation |
|
|
| `pkg/protocol/nip43/types.go` | NIP-43 invite management |
|
|
|
|
### Application Layer Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `app/server.go` | HTTP/WebSocket server setup |
|
|
| `app/listener.go` | Connection aggregate |
|
|
| `app/handle-event.go` | EVENT message handler |
|
|
| `app/handle-req.go` | REQ message handler |
|
|
| `app/handle-auth.go` | AUTH message handler |
|
|
| `app/handle-nip43.go` | NIP-43 membership handlers |
|
|
|
|
### Infrastructure Files
|
|
|
|
| File | Purpose |
|
|
|------|---------|
|
|
| `pkg/database/database.go` | Badger implementation |
|
|
| `pkg/neo4j/` | Neo4j implementation |
|
|
| `pkg/wasmdb/` | WasmDB implementation |
|
|
| `pkg/ratelimit/limiter.go` | Rate limiting |
|
|
| `pkg/sync/manager.go` | Distributed sync |
|
|
|
|
---
|
|
|
|
*Generated: 2025-12-23*
|
|
*Analysis based on ORLY codebase v0.36.10*
|