38 KiB
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 | Status |
|---|---|---|---|---|
| 1 | Formalize Domain Events | High | Medium | Pending |
| 2 | Strengthen Aggregate Boundaries | High | Medium | Partial |
| 3 | Extract Application Services | Medium | High | Pending |
| 4 | Establish Ubiquitous Language Glossary | Medium | Low | Pending |
| 5 | Add Domain-Specific Error Types | Medium | Low | Pending |
| 6 | Enforce Value Object Immutability | Low | Low | Addressed |
| 7 | Document Context Map | Medium | Low | This Document |
Table of Contents
- Executive Summary
- Strategic Design Analysis
- Tactical Design Analysis
- Anti-Patterns Identified
- Detailed Recommendations
- Implementation Checklist
- Appendix: File References
Executive Summary
ORLY demonstrates mature DDD adoption with sophisticated modular architecture. The codebase has evolved from a single-binary relay to a multi-process system with clear bounded context separation, gRPC-based inter-process communication, and pluggable implementations for database, ACL, and sync services.
Strengths:
- Clear separation between
app/(application layer) andpkg/(domain/infrastructure) - Repository pattern with four interchangeable backends (Badger, Neo4j, WasmDB, gRPC)
- Interface-based ACL system with four implementations (None, Follows, Managed, Curating)
- Driver Registry pattern for runtime component selection
- Process Supervisor pattern for split IPC mode deployment
- Per-connection aggregate isolation in
Listener - Strong use of Go interfaces for dependency inversion
- Immutable
EventRefvalue object alongside legacyIdPkTs - Comprehensive protocol extensions (NIP-43, NIP-77, NIP-86, Blossom, Graph Queries)
- gRPC service layer exposing all major interfaces for remote access
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: 8/10 (improved from 7.5/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 with indexing and querying
- Key Abstractions:
Databaseinterface (109 lines),Subscription,Payment,NIP43Membership - Implementations: Badger (embedded), Neo4j (graph), WasmDB (browser), gRPC (remote)
- gRPC Server:
pkg/database/server/- DatabaseService with 250+ RPC methods - File:
pkg/database/interface.go:17-109
2. Access Control Context (pkg/acl/)
- Responsibility: Authorization decisions for read/write operations
- Key Abstractions:
Iinterface,Registry, access levels (none/read/write/admin/owner) - Implementations:
None,Follows,Managed,Curating - gRPC Server:
pkg/acl/server/- ACLService - Files:
pkg/acl/acl.go,pkg/interfaces/acl/acl.go:21-40
3. Event Policy Context (pkg/policy/)
- Responsibility: Event filtering, validation, rate limiting rules, follows-based whitelisting
- Key Abstractions:
Rule,Kinds,P(PolicyManager) - Invariants: Whitelist/blacklist precedence, size limits, tag requirements, protected events
- File:
pkg/policy/policy.go(extensive, ~1000 lines)
4. Connection Management Context (app/)
- Responsibility: WebSocket lifecycle, message routing, authentication, flow control
- Key Abstractions:
Listener,Server, message handlers,messageRequest - Handlers: 25+ message type 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/): Invite-based access control (kinds 28934, 28936, 8000) - Graph Queries (
pkg/protocol/graph/): BFS traversal for follows/followers/threads - NWC Payments (
pkg/protocol/nwc/): Nostr Wallet Connect integration - Blossom (
pkg/protocol/blossom/): BUD protocol definitions - Directory (
pkg/protocol/directory/): Relay directory client
- NIP-43 Membership (
6. Blob Storage Context (pkg/blossom/)
- Responsibility: Binary blob storage following BUD-09 specifications
- Key Abstractions:
Server,Storage,Blob,BlobMeta - Invariants: SHA-256 hash integrity, MIME type validation, quota enforcement
- Files:
pkg/blossom/server.go,pkg/blossom/storage.go
7. Rate Limiting Context (pkg/ratelimit/)
- Responsibility: Adaptive throttling based on system load using PID controller
- Key Abstractions:
Limiter,Config,OperationType(Read/Write) - Integration: Memory pressure from database backends via
loadmonitorinterface - File:
pkg/ratelimit/limiter.go
8. Distributed Sync Context (pkg/sync/)
- Responsibility: Federation and replication between relay peers
- Key Abstractions:
Manager,Registry, driver pattern - Implementations:
- Negentropy (
pkg/sync/negentropy/): NIP-77 set reconciliation - Cluster (
pkg/sync/cluster/): HTTP-based pull replication (kind 39108) - Distributed (
pkg/sync/distributed/): Peer-to-peer sync - RelayGroup (
pkg/sync/relaygroup/): Relay grouping (kind 39105)
- Negentropy (
- gRPC Clients: Each sync implementation has gRPC client for split mode
- Files:
pkg/sync/manager.go,pkg/sync/negentropy/,pkg/sync/cluster/
9. Spider Context (pkg/spider/)
- Responsibility: Syncing events from admin relays for followed pubkeys
- Key Abstractions:
Spider,RelayConnection,DirectorySpider - Integration: Batch subscriptions, rate limit backoff, blackout periods
- File:
pkg/spider/spider.go
10. Process Supervision Context (cmd/orly-launcher/) NEW
- Responsibility: Multi-process lifecycle management for split IPC mode
- Key Abstractions:
Supervisor,Config, process state machine - Patterns: Supervisor pattern with dependency ordering, crash recovery
- Integration: gRPC health checks, graceful shutdown ordering
- Files:
cmd/orly-launcher/supervisor.go,cmd/orly-launcher/config.go
11. Certificate Management Context (cmd/orly-certs/) NEW
- Responsibility: TLS certificate provisioning and renewal
- Key Abstractions: Certificate manager, DNS-01 challenge handler
- Integration: Independent service, communicates via filesystem
- File:
cmd/orly-certs/
Context Map
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Process Supervision (orly-launcher) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Supervisor │───▶│ DB Process │───▶│ ACL Process │───▶│Relay Process│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │ │ │ │ │
│ │ [Dependency] │ [gRPC] │ [gRPC] │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │Sync Services│ │ Certs │ │ Admin Web │ │ Health │ │
│ │ (4 procs) │ │ Service │ │ UI │ │ Checks │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Event Storage │ │ Access Control│ │ Event Policy │
│ (pkg/database/)│ │ (pkg/acl/) │ │ (pkg/policy/) │
│ │ │ │ │ │
│ Badger│Neo4j │◀─[gRPC]─────▶│Follows│Managed │◀─[Conformist]│ Manager │
│ WasmDB│gRPC │ │Curating│None │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
│ │ │
│ [Shared Kernel] │ │
▼ ▼ │
┌────────────────────────────────────────────────────────────┐ │
│ Event Entity │ │
│ (git.mleku.dev/mleku/nostr) │◀─────────┘
│ Filter, Tag, Subscription types │
└────────────────────────────────────────────────────────────┘
│ │
│ [Customer-Supplier] │ [Customer-Supplier]
▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Blob Storage │ │ Protocol │ │ Rate Limiting │
│ (pkg/blossom) │ │ Extensions │ │ (pkg/ratelimit)│
│ │ │ (pkg/protocol/)│ │ │
└────────────────┘ └────────────────┘ └────────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ NIP-43 │ │ NIP-77 │ │ Graph Queries │
│ Membership │ │ Negentropy │ │(pkg/protocol/ │
│(kinds 28934/36)│ │ Sync │ │ graph/) │
└────────────────┘ └────────────────┘ └────────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Cluster │ │ Distributed │ │ Relay Group │
│ Sync │ │ Sync │ │ (kind 39105) │
│ (kind 39108) │ │ │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
Integration Patterns Identified:
| Upstream | Downstream | Pattern | Notes |
|---|---|---|---|
| nostr library | All contexts | Shared Kernel | Event, Filter, Tag types |
| Database | ACL, Policy, Blossom, Sync | Customer-Supplier | Query for follows, permissions, event storage |
| Policy | Handlers, Sync | Conformist | All respect policy decisions |
| ACL | Handlers, Blossom | Conformist | Handlers/Blossom respect access levels |
| Rate Limit | Database | Anti-Corruption | LoadMonitor abstraction |
| Sync Services | Database | Customer-Supplier | Serial-based event replication |
| Launcher | All Services | Supervisor | Process lifecycle management |
| gRPC Layer | DB, ACL, Sync | Published Language | Protocol buffer contracts |
Subdomain Classification
| Subdomain | Type | Justification |
|---|---|---|
| Event Storage | Core | Central to relay's value proposition |
| Access Control | Core | Key differentiator (WoT, follows-based, managed, curating) |
| Event Policy | Core | Enables complex filtering rules |
| Graph Queries | Core | Unique social graph traversal capabilities |
| NIP-43 Membership | Core | Unique invite-based access model |
| Blob Storage (Blossom) | Core | Media hosting differentiator |
| NIP-77 Negentropy Sync | Core | Efficient relay-to-relay sync |
| Connection Management | Supporting | Standard WebSocket infrastructure |
| Rate Limiting | Supporting | Operational concern with PID controller |
| Cluster Sync | Supporting | Infrastructure for federation |
| Spider | Supporting | Data aggregation from external relays |
| Process Supervision | Generic | Standard process management |
| Certificate Management | Generic | Standard TLS infrastructure |
Tactical Design Analysis
Entities
Entities are objects with identity that persists across state changes.
Listener (Connection Entity)
// 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
messageQueue chan messageRequest // Async message processing
droppedMessages atomic.Int64 // Flow control counter
// ... more fields
}
- Identity: WebSocket connection pointer
- Lifecycle: Created on connect, destroyed on disconnect
- Invariants: Only one authenticated pubkey per connection; AUTH processed synchronously
InviteCode (NIP-43 Entity)
// 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)
// pkg/database/interface.go (implied by methods)
// GetSubscription, ExtendSubscription, RecordPayment
- Identity: Pubkey
- Lifecycle: Trial -> Active -> Expired
- Invariants: Can only extend if not expired
Blob (Blossom Entity)
// pkg/blossom/blob.go (implied)
type BlobMeta struct {
SHA256 string // Identity: content-addressable
Size int64
Type string // MIME type
Uploaded time.Time
Owner []byte // Uploader pubkey
}
- Identity: SHA-256 hash
- Lifecycle: Uploaded -> Active -> Deleted
- Invariants: Hash must match content; owner can delete
Process (Supervisor Entity) NEW
// cmd/orly-launcher/supervisor.go (implied)
type Process struct {
Name string // Identity: service name
Cmd *exec.Cmd // Running process
State ProcessState
Restarts int
}
- Identity: Service name (orly-db, orly-acl, etc.)
- Lifecycle: Stopped -> Starting -> Running -> Stopping
- Invariants: Dependency ordering; health check before dependent startup
Value Objects
Value objects are immutable and defined by their attributes, not identity.
EventRef (Immutable Event Reference)
// pkg/interfaces/store/store_interface.go:99-107
type EventRef struct {
id ntypes.EventID // 32 bytes
pub ntypes.Pubkey // 32 bytes
ts int64 // 8 bytes
ser uint64 // 8 bytes
}
- Equality: By all fields (fixed-size arrays)
- Immutability: Unexported fields, accessor methods return copies
- Size: 80 bytes, cache-line friendly, stack-allocated
IdPkTs (Legacy Event Reference)
// pkg/interfaces/store/store_interface.go:67-72
type IdPkTs struct {
Id []byte // Event ID
Pub []byte // Pubkey
Ts int64 // Timestamp
Ser uint64 // Serial number
}
- Equality: By all fields
- Issue: Mutable slices (use
ToEventRef()for immutable version) - Migration: Has
ToEventRef()and accessorsIDFixed(),PubFixed()
Kinds (Policy Specification)
// pkg/policy/policy.go:58-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)
// pkg/policy/policy.go:75-180
type Rule struct {
Description string
WriteAllow []string
WriteDeny []string
ReadFollowsWhitelist []string
WriteFollowsWhitelist []string
MaxExpiryDuration string
SizeLimit *int64
ContentLimit *int64
Privileged bool
ProtectedRequired bool
ReadAllowPermissive bool
WriteAllowPermissive bool
// ... binary caches
}
- Complexity: 25+ fields, decomposition candidate
- Binary caches: Performance optimization for hex->binary conversion
WriteRequest (Message Value)
// pkg/protocol/publish/types.go
type WriteRequest struct {
Data []byte
MsgType int
IsControl bool
IsPing bool
Deadline time.Time
}
LauncherConfig (Configuration Value) NEW
// cmd/orly-launcher/config.go
type Config struct {
DBBackend string // badger, neo4j
DBListen string // 127.0.0.1:50051
ACLEnabled bool
ACLMode string // follows, managed, curation
ACLListen string // 127.0.0.1:50052
AdminEnabled bool
AdminPort int
AdminOwners []string
// ... sync service configs
}
- Persistence: JSON file at
~/.config/orly/launcher.json - Environment Override: All fields can be overridden via
ORLY_LAUNCHER_*
Aggregates
Aggregates are clusters of entities/value objects with consistency boundaries.
Listener Aggregate
- Root:
Listener - Members: Subscriptions map, auth state, write channel, message queue
- Boundary: Per-connection isolation
- Invariants:
- Subscriptions must exist before receiving matching events
- AUTH must complete before other messages check authentication
- Message processing uses RWMutex for pause/resume during policy updates
// app/listener.go:226-249 - 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 (configurable per-kind)
- Validation:
app/handle-event.go
InviteCode Aggregate
- Root:
InviteCode - Members: Code, expiry, usage tracking
- Invariants:
- Code uniqueness
- Single-use enforcement
- Expiry validation
Blossom Blob Aggregate
- Root:
BlobMeta - Members: Content data, metadata, owner
- Invariants:
- SHA-256 integrity
- Size limits
- MIME type restrictions
- Owner-only deletion
Supervisor Aggregate NEW
- Root:
Supervisor - Members: Process map, config, dependency graph
- Boundary: All managed processes
- Invariants:
- Dependency ordering (DB -> ACL -> Sync -> Relay)
- Health checks before dependent startup
- Reverse shutdown ordering
- Crash recovery with restart limits
Repositories
The Repository pattern abstracts persistence for aggregate roots.
Database Interface (Primary Repository)
// pkg/database/interface.go:17-109
type Database interface {
// Core lifecycle
Path() string
Init(path string) error
Sync() error
Close() error
Ready() <-chan struct{}
// Event persistence (30+ methods)
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)
// Blossom integration
ExtendBlossomSubscription(pubkey []byte, tier string, storageMB int64, daysExtended int) error
GetBlossomStorageQuota(pubkey []byte) (quotaMB int64, err error)
// Query cache
GetCachedJSON(f *filter.F) ([][]byte, bool)
CacheMarshaledJSON(f *filter.F, marshaledJSON [][]byte)
}
Repository Implementations:
- Badger (
pkg/database/database.go): Embedded key-value store - Neo4j (
pkg/neo4j/): Graph database for social queries - WasmDB (
pkg/wasmdb/): Browser IndexedDB for WASM builds - gRPC (
pkg/database/grpc/): Remote database via gRPC NEW
Interface Segregation:
// pkg/interfaces/store/store_interface.go:21-38
type I interface {
Pather
io.Closer
Wiper
Querier // QueryForIds
Querent // QueryEvents
Deleter // DeleteEvent
Saver // SaveEvent
Importer
Exporter
Syncer
LogLeveler
EventIdSerialer
Initer
SerialByIder
}
Driver Registry Pattern NEW
// pkg/database/ - Driver selection at runtime
database.HasDriver("badger") // Check availability
database.NewFromDriver("badger", config) // Create instance
// pkg/acl/ - ACL driver selection
acl.HasDriver("follows")
acl.NewACLFromDriver("follows", config)
// pkg/sync/ - Sync driver selection
sync.HasDriver("negentropy")
sync.NewSyncManager("negentropy", config)
Domain Services
Domain services encapsulate logic that doesn't belong to any single entity.
ACL Registry (Access Decision Service)
// pkg/acl/acl.go:40-48
func (s *S) GetAccessLevel(pub []byte, address string) (level string)
func (s *S) CheckPolicy(ev *event.E) (allowed bool, err error)
func (s *S) AddFollow(pub []byte)
- Delegates to active ACL implementation
- Stateless decision based on pubkey and IP
- Optional
PolicyCheckerinterface for custom validation
Policy Manager (Event Validation Service)
// pkg/policy/policy.go (P type)
// CheckPolicy evaluates rule chains, scripts, whitelist/blacklist logic
// Supports per-kind rules with follows-based whitelisting
- Complex rule evaluation logic
- Script execution for custom validation
- Binary cache optimization for pubkey comparisons
InviteManager (Invite Lifecycle Service)
// 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
Graph Executor (Query Execution Service)
// pkg/protocol/graph/executor.go:56-60
type Executor struct {
db GraphDatabase
relaySigner signer.I
relayPubkey []byte
}
func (e *Executor) Execute(q *Query) (*event.E, error)
- BFS traversal for follows/followers/threads
- Generates relay-signed ephemeral response events
Rate Limiter (Throttling Service)
// pkg/ratelimit/limiter.go
type Limiter struct { ... }
func (l *Limiter) Wait(ctx context.Context, op OperationType) error
- PID controller-based adaptive throttling
- Separate setpoints for read/write operations
- Emergency mode with hysteresis
Supervisor (Process Lifecycle Service) NEW
// cmd/orly-launcher/supervisor.go
type Supervisor struct {
config *Config
processes map[string]*Process
mu sync.Mutex
}
func (s *Supervisor) Start() error
func (s *Supervisor) Stop() error
func (s *Supervisor) Restart(name string) error
func (s *Supervisor) IsRunning() bool
- Manages process lifecycle with dependency ordering
- Health checks via gRPC before proceeding
- Crash recovery with configurable restart limits
- Graceful shutdown in reverse dependency order
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, kind 8000 event |
| MembershipRemoved | NIP-43 leave request | ACL update, kind 8001 event |
| PolicyUpdated | Policy config event | messagePauseMutex.Lock() |
| BlobUploaded | Blossom PUT success | Quota updated |
| BlobDeleted | Blossom DELETE | Quota released |
| ProcessStarted | Supervisor start | Health check, dependency unlock |
| ProcessCrashed | Process exit | Restart attempt, alert |
| ConfigUpdated | Admin UI save | JSON persistence, reload |
Anti-Patterns Identified
1. Large Handler Methods (Partial Anemic Domain Model)
Location: app/handle-event.go (600+ lines)
Issue: The event handling contains:
- Input validation (lowercase hex, JSON structure)
- Policy checking
- ACL verification
- Signature verification
- Persistence
- Event delivery
- Special case handling (delete, ephemeral, NIP-43, NIP-86)
Impact: Difficult to test, maintain, and understand. Business rules are embedded in orchestration code.
2. Mutable Value Object Fields (Partially Addressed)
Location: pkg/interfaces/store/store_interface.go:67-72
type IdPkTs struct {
Id []byte // Mutable slice
Pub []byte // Mutable slice
Ts int64
Ser uint64
}
Mitigation: New EventRef type with unexported fields provides immutable alternative.
Use ToEventRef() method for safe conversion.
3. Global Singleton Registry
Location: pkg/acl/acl.go:10
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 with binary caches, suggesting decomposition into:
AccessRule(allow/deny lists, follows whitelists)SizeRule(limits)TimeRule(expiry, age)ValidationRule(tags, regex, protected)
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.
// 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
}
type BlobUploaded struct {
SHA256 string
Owner []byte
Size int64
Timestamp time.Time
}
type ProcessStateChanged struct {
ServiceName string
OldState ProcessState
NewState ProcessState
Timestamp time.Time
}
2. Strengthen Aggregate Boundaries
Problem: Aggregate internals are exposed via public fields.
Solution: The Listener already uses behavior methods well. Extend pattern:
func (l *Listener) IsAuthenticated() bool {
return len(l.authedPubkey.Load()) > 0
}
func (l *Listener) AuthenticatedPubkey() []byte {
return l.authedPubkey.Load()
}
3. Extract Application Services
Problem: Handler methods contain mixed concerns.
Solution: Extract domain logic into focused application services.
// 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)
4. Establish Ubiquitous Language Glossary
Problem: Terminology is inconsistent across the codebase.
Current Inconsistencies:
- "subscription" (payment) vs "subscription" (REQ filter)
- "pub" vs "pubkey" vs "author"
- "spider" vs "sync" for relay federation
Solution: Maintain a GLOSSARY.md:
# ORLY Ubiquitous Language
| 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 | `acl.Level` |
| Policy | Event validation rules | `policy.Rule` |
| Blob | Binary content (images, media) | `blossom.BlobMeta` |
| Spider | Event aggregator from external relays | `spider.Spider` |
| Sync | Peer-to-peer replication | `sync.Manager` |
| Supervisor | Process lifecycle manager | `supervisor.Supervisor` |
| Driver | Pluggable implementation | Registry pattern |
5. Add Domain-Specific Error Types
Problem: Errors are strings or generic types.
Solution: Create typed domain errors in pkg/interfaces/neterr/ pattern:
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"}
ErrBlobTooLarge = &DomainError{Code: "BLOB_TOO_LARGE"}
ErrServiceUnavailable = &DomainError{Code: "SERVICE_UNAVAILABLE"}
)
6. Enforce Value Object Immutability - ADDRESSED
The EventRef type now provides an immutable alternative:
// pkg/interfaces/store/store_interface.go:99-153
type EventRef struct {
id ntypes.EventID // unexported
pub ntypes.Pubkey // unexported
ts int64
ser uint64
}
func (r EventRef) ID() ntypes.EventID { return r.id } // Returns copy
func (r EventRef) IDHex() string { return r.id.Hex() }
func (i *IdPkTs) ToEventRef() EventRef // Migration path
7. Document Context Map - THIS DOCUMENT
The context map is now documented in this file with integration patterns.
Implementation Checklist
Currently Satisfied
- Bounded contexts identified with clear boundaries
- Repositories abstract persistence for aggregate roots
- Multiple repository implementations (Badger/Neo4j/WasmDB/gRPC)
- Interface segregation prevents circular dependencies
- Configuration centralized (
app/config/config.go) - Per-connection aggregate isolation
- Access control as pluggable strategy pattern
- Value objects have immutable alternative (
EventRef) - Context map documented
- Driver registry pattern for runtime selection
- gRPC layer for distributed deployment
- Process supervision for split mode
Needs Attention
- Ubiquitous language documented and used consistently
- Domain events capture important state changes (explicit types)
- Entities have behavior, not just data (more encapsulation)
- No business logic in application services (handler decomposition)
- No infrastructure concerns in domain layer
Appendix: File References
Core Domain Files
| File | Purpose |
|---|---|
pkg/database/interface.go |
Repository interface (109 lines) |
pkg/interfaces/acl/acl.go |
ACL interface definition with PolicyChecker |
pkg/interfaces/store/store_interface.go |
Store sub-interfaces, IdPkTs, EventRef |
pkg/policy/policy.go |
Policy rules and evaluation (~1000 lines) |
pkg/protocol/nip43/types.go |
NIP-43 invite management |
pkg/protocol/graph/executor.go |
Graph query execution |
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 |
app/handle-nip86.go |
NIP-86 management handlers |
app/handle-negentropy.go |
NIP-77 negentropy sync |
app/handle-policy-config.go |
Policy configuration events |
Infrastructure Files
| File | Purpose |
|---|---|
pkg/database/database.go |
Badger implementation |
pkg/database/server/ |
gRPC database server |
pkg/database/grpc/ |
gRPC database client |
pkg/neo4j/ |
Neo4j implementation |
pkg/wasmdb/ |
WasmDB implementation |
pkg/blossom/server.go |
Blossom blob storage server |
pkg/ratelimit/limiter.go |
PID-based rate limiting |
pkg/sync/negentropy/ |
NIP-77 negentropy sync |
pkg/sync/cluster/ |
HTTP cluster replication |
pkg/spider/spider.go |
Event spider/aggregator |
Command Binaries
| Binary | Purpose |
|---|---|
cmd/orly-launcher/ |
Process supervisor with admin UI |
cmd/orly-db-badger/ |
Standalone Badger database server |
cmd/orly-db-neo4j/ |
Standalone Neo4j database server |
cmd/orly-acl-follows/ |
Follows-based ACL server |
cmd/orly-acl-managed/ |
NIP-86 managed ACL server |
cmd/orly-acl-curation/ |
Trust-tier curation ACL server |
cmd/orly-sync-negentropy/ |
NIP-77 negentropy sync service |
cmd/orly-sync-cluster/ |
Cluster replication service |
cmd/orly-certs/ |
Certificate management service |
Interface Packages
| Package | Purpose |
|---|---|
pkg/interfaces/acl/ |
ACL abstraction |
pkg/interfaces/loadmonitor/ |
Load monitoring abstraction |
pkg/interfaces/neterr/ |
Network error types |
pkg/interfaces/pid/ |
PID controller interface |
pkg/interfaces/policy/ |
Policy interface |
pkg/interfaces/publisher/ |
Event publisher interface |
pkg/interfaces/resultiter/ |
Result iterator interface |
pkg/interfaces/store/ |
Store interface with IdPkTs, EventRef |
pkg/interfaces/typer/ |
Type introspection interface |
Protocol Buffer Definitions
| Package | Purpose |
|---|---|
pkg/proto/orlydb/v1/ |
Database gRPC service (250+ methods) |
pkg/proto/orlyacl/v1/ |
ACL gRPC service |
pkg/proto/orlysync/ |
Sync services (negentropy, cluster, etc.) |
Generated: 2026-01-24 Analysis based on ORLY codebase v0.56.4