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.
Analysis Version: v0.56.8 Last Updated: 2026-01-24
| # | Recommendation | Impact | Effort | Status |
|---|---|---|---|---|
| 1 | Domain Events & Dispatcher | High | Medium | Implemented |
| 2 | Domain-Specific Error Types | Medium | Low | Implemented |
| 3 | Application Service Extraction | Medium | High | Implemented |
| 4 | Value Object Immutability | Low | Low | Implemented |
| 5 | Ubiquitous Language Glossary | Medium | Low | Implemented |
| 6 | Aggregate Boundary Strengthening | High | Medium | Implemented |
| 7 | Document Context Map | Medium | Low | This Document |
| 8 | Handler Simplification | Medium | Medium | Implemented |
| 9 | Injectable ACL Registry | Medium | Low | Implemented |
| 10 | Rule Value Object Decomposition | Low | Medium | Implemented |
- Bounded Contexts - Context Map - Subdomain Classification
- Entities - Value Objects - Aggregates - Repositories - Domain Services - Domain Events - Error Handling
ORLY demonstrates exemplary 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.
Recent Improvements (v0.56.5-v0.56.8):
Strengths:
app/ (application layer) and pkg/ (domain/infrastructure)ListenerEventRef and stack-optimized IdPkTs value objectsORLY organizes code into 11 distinct bounded contexts, each with its own model and language:
pkg/database/)Database interface, Subscription, Payment, NIP43Membershippkg/database/server/ - DatabaseService with 250+ RPC methodspkg/acl/)I interface, Registry, access levels (none/read/write/admin/owner)None, Follows, Managed, Curatingpkg/acl/server/ - ACLServicepkg/policy/)Rule, Kinds, P (PolicyManager)pkg/event/) NEW - validation.Service - Multi-layer validation (raw JSON, signature, timestamp)
- authorization.Service - Authorization decisions combining ACL + policy
- routing.Router - Kind-based routing with handler registration
- processing.Service - Event persistence and delivery
- specialkinds.Registry - Extensible special kind handling
- ingestion.Service - Full pipeline orchestration
app/)Listener, Server, message handlers, messageRequestpkg/domain/) NEW - events.DomainEvent - Base event interface
- events.Dispatcher - Pub/sub with sync/async publishing
- errors.DomainError - Typed error categories
pkg/protocol/) - NIP-43 Membership (pkg/protocol/nip43/): Invite-based access control
- Graph Queries (pkg/protocol/graph/): BFS traversal for social graphs
- NWC Payments (pkg/protocol/nwc/): Nostr Wallet Connect
- Blossom (pkg/protocol/blossom/): BUD protocol definitions
pkg/blossom/)Server, Storage, Blob, BlobMetapkg/ratelimit/)Limiter, Config, OperationTypepkg/sync/)cmd/orly-launcher/)Supervisor, Config, process state machine┌─────────────────────────────────────────────────────────────────────────────────┐
│ Process Supervision (orly-launcher) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Supervisor │───▶│ DB Process │───▶│ ACL Process │───▶│Relay Process│ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────────┼───────────────────────────────┐
│ │ │
▼ ▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 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]
▼ ▼
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ Event │ │ Domain │ │ Rate Limiting │
│ Processing │◀────────────▶│ Events │ │ (pkg/ratelimit)│
│ (pkg/event/) │ │(pkg/domain/) │ │ │
└────────────────┘ └────────────────┘ └────────────────┘
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 |
| Policy | Handlers, Sync | Conformist | All respect policy decisions |
| Domain Events | Processing, ACL | Pub/Sub | Event dispatcher decouples contexts |
| gRPC Layer | DB, ACL, Sync | Published Language | Protocol buffer contracts |
| 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 |
| Event Processing | Core | Clean orchestration of event pipeline |
| Graph Queries | Core | Unique social graph traversal capabilities |
| NIP-43 Membership | Core | Unique invite-based access model |
| Blob Storage (Blossom) | Core | Media hosting differentiator |
| Domain Events | Supporting | Cross-cutting concern infrastructure |
| Connection Management | Supporting | Standard WebSocket infrastructure |
| Rate Limiting | Supporting | Operational concern with PID controller |
| Process Supervision | Generic | Standard process management |
Entities are objects with identity that persists across state changes.
// app/listener.go:24-52
type Listener struct {
conn *websocket.Conn // Identity: connection handle
connID string // Unique identifier
challenge atomicutils.Bytes // Auth challenge state
authedPubkey atomicutils.Bytes // Authenticated identity
subscriptions map[string]context.CancelFunc
messageQueue chan messageRequest // Async message processing
}
// pkg/protocol/nip43/
type InviteCode struct {
Code string // Identity: unique code
ExpiresAt time.Time
UsedBy []byte // Tracks consumption
CreatedAt time.Time
}
// cmd/orly-launcher/supervisor.go
type Process struct {
Name string // Identity: service name
Cmd *exec.Cmd // Running process
State ProcessState // Stopped/Starting/Running/Stopping
Restarts int // Crash counter
}
Value objects are immutable and defined by their attributes, not identity.
// pkg/interfaces/store/store_interface.go:99-153
type EventRef struct {
id ntypes.EventID // 32 bytes, unexported
pub ntypes.Pubkey // 32 bytes, unexported
ts int64 // 8 bytes
ser uint64 // 8 bytes
}
// Total: 80 bytes - fits in L1 cache line
// pkg/interfaces/store/store_interface.go:67-97
type IdPkTs struct {
Id ntypes.EventID // [32]byte - fixed array, stack-allocated
Pub ntypes.Pubkey // [32]byte - fixed array, stack-allocated
Ts int64
Ser uint64
}
// Constructor copies slices into fixed arrays
func NewIdPkTs(id, pub []byte, ts int64, ser uint64) IdPkTs
// Accessors for slice views when needed
func (i *IdPkTs) IDSlice() []byte { return i.Id[:] }
func (i *IdPkTs) PubSlice() []byte { return i.Pub[:] }
// pkg/event/authorization/authorization.go:10-20
type Decision struct {
Allowed bool
AccessLevel string // none/read/write/admin/owner/blocked/banned
IsAdmin bool
IsOwner bool
IsPeerRelay bool
SkipACLCheck bool
DenyReason string
RequireAuth bool
}
// pkg/policy/policy.go:58-63
type Kinds struct {
Whitelist []int
Blacklist []int
}
// pkg/policy/policy.go:79-174
type AccessControl struct {
WriteAllow []string // Pubkeys allowed to write
WriteDeny []string // Pubkeys denied write
ReadAllow []string // Pubkeys allowed to read
ReadDeny []string // Pubkeys denied read
WriteAllowFollows bool // Grant access to admin follows
ReadFollowsWhitelist []string
WriteFollowsWhitelist []string
ReadAllowPermissive bool
WriteAllowPermissive bool
}
type Constraints struct {
MaxExpiry *int64 // Maximum expiry time
MaxExpiryDuration string // ISO-8601 duration
SizeLimit *int64 // Max serialized size
ContentLimit *int64 // Max content size
RateLimit *int64 // Write rate limit
MaxAgeOfEvent *int64 // Max age for created_at
MaxAgeEventInFuture *int64 // Max future offset
ProtectedRequired bool // Require NIP-70 "-" tag
Privileged bool // Restrict to authenticated parties
}
type TagValidationConfig struct {
MustHaveTags []string // Required tag keys
TagValidation map[string]string // Tag -> regex pattern
IdentifierRegex string // Regex for "d" tags
}
type Rule struct {
Description string
Script string
AccessControl // Embedded - who can access
Constraints // Embedded - what limits apply
TagValidationConfig // Embedded - tag requirements
}
Aggregates are clusters of entities/value objects with consistency boundaries.
Listener- Subscriptions must exist before receiving matching events - AUTH must complete before authenticated operations - Message processing uses RWMutex for pause/resume during policy updates
event.E (from nostr library)- ID must match computed hash - Signature must be valid - Timestamp must be within bounds
Supervisor- Dependency ordering (DB -> ACL -> Sync -> Relay) - Health checks before dependent startup - Reverse shutdown ordering
The Repository pattern abstracts persistence for aggregate roots.
// pkg/database/interface.go:17-109
type Database interface {
// Core lifecycle
Init(path string) error
Close() error
Ready() <-chan struct{}
// 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
// Membership management
AddNIP43Member(pubkey []byte, inviteCode string) error
IsNIP43Member(pubkey []byte) (isMember bool, err error)
// ... 30+ more methods
}
Repository Implementations:
| Backend | Type | Use Case | Location |
|---|---|---|---|
| Badger | LSM Tree | Primary - high throughput | pkg/database/ |
| Neo4j | Property Graph | Social queries | pkg/neo4j/ |
| WasmDB | IndexedDB | Browser WASM | pkg/wasmdb/ |
| gRPC | Remote service | Split IPC mode | pkg/database/grpc/ |
// Runtime driver selection
database.HasDriver("badger")
database.NewFromDriver("badger", config)
acl.HasDriver("follows")
acl.NewACLFromDriver("follows", config)
Domain services encapsulate logic that doesn't belong to any single entity.
// pkg/event/validation/validation.go
type Service struct {
cfg *Config
}
func (s *Service) ValidateRawJSON(msg []byte) Result
func (s *Service) ValidateEvent(ev *event.E) Result
func (s *Service) ValidateProtectedTag(ev *event.E, authedPubkey []byte) Result
// pkg/event/authorization/authorization.go
type Service struct {
cfg *Config
acl ACLRegistry
policy PolicyManager
sync SyncManager
}
func (s *Service) Authorize(ev *event.E, authedPubkey []byte, remote string, kind uint16) Decision
// pkg/event/routing/routing.go
type Router interface {
Route(ev *event.E, authedPubkey []byte) Result
Register(kind uint16, handler Handler)
RegisterKindCheck(name string, check func(uint16) bool, handler Handler)
}
// pkg/event/processing/processing.go
type Service struct {
db Database
publishers *publisher.P
eventDispatch *events.Dispatcher
}
func (s *Service) Process(ctx context.Context, ev *event.E, authedPubkey []byte) Result
// pkg/event/ingestion/service.go
type Service struct {
validator *validation.Service
authorizer *authorization.Service
router routing.Router
processor *processing.Service
sprocketChecker SprocketChecker
specialKinds *specialkinds.Registry
}
func (s *Service) Ingest(ctx context.Context, ev *event.E, connCtx *ConnectionContext) Result
// pkg/acl/acl.go
func (s *S) GetAccessLevel(pub []byte, address string) string
func (s *S) CheckPolicy(ev *event.E) (bool, error)
// pkg/policy/policy.go
func (p *P) CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error)
Status: IMPLEMENTED (v0.56.5)
// pkg/domain/events/events.go
type DomainEvent interface {
OccurredAt() time.Time
EventType() string
}
type Base struct {
occurredAt time.Time
eventType string
}
// Event Storage Events
type EventSaved struct {
Base
Event *event.E
Serial uint64
IsAdmin bool
IsOwner bool
}
type EventDeleted struct {
Base
EventID []byte
DeletedBy []byte
Serial uint64
}
// Access Control Events
type FollowListUpdated struct {
Base
AdminPubkey []byte
AddedFollows [][]byte
RemovedFollows [][]byte
}
type ACLMembershipChanged struct {
Base
Pubkey []byte
PrevLevel string
NewLevel string
Reason string
}
// Policy Events
type PolicyConfigUpdated struct {
Base
UpdatedBy []byte
Changes map[string]interface{}
}
// Connection Events
type ConnectionOpened struct {
Base
ConnID string
Remote string
}
// Authentication Events
type UserAuthenticated struct {
Base
Pubkey []byte
ConnID string
Method string
}
// Plus 10+ more event types...
// pkg/domain/events/dispatcher.go
type Subscriber interface {
Handle(event DomainEvent)
Supports(eventType string) bool
}
type Dispatcher struct {
subscribers []Subscriber
asyncChan chan DomainEvent
}
func (d *Dispatcher) Publish(event DomainEvent) // Sync
func (d *Dispatcher) PublishAsync(event DomainEvent) // Async
func (d *Dispatcher) Subscribe(s Subscriber)
Status: IMPLEMENTED (v0.56.5)
// pkg/domain/errors/errors.go
type DomainError interface {
error
Code() string // e.g., "INVALID_ID"
Category() string // e.g., "validation"
IsRetryable() bool
}
// Error Categories
type ValidationError struct { Base; Field string }
type AuthorizationError struct { Base; Pubkey []byte; AccessLevel string; RequireAuth bool }
type ProcessingError struct { Base; EventID []byte; Kind uint16 }
type PolicyError struct { Base; RuleName string; Action string }
type StorageError struct { Base }
type ServiceError struct { Base; ServiceName string }
func Is(err error, target DomainError) bool
func Code(err error) string
func Category(err error) string
func IsRetryable(err error) bool
func NeedsAuth(err error) bool
Location: app/handle-event.go
Status: Handlers are now thin protocol adapters. The HandleEvent method delegates to the ingestion service which orchestrates validation, authorization, routing, and processing. Connection-specific concerns (NIP-43, policy config) remain in handlers where they belong.
Location: pkg/acl/acl.go:10
Status: ACL registry is now injectable via the acliface.Registry interface. The Server struct holds an aclRegistry field with an accessor method ACLRegistry(). Tests and main.go inject the registry during construction. The global Registry variable remains for backward compatibility but is not required.
Status: Event infrastructure is now fully wired up. The processing service dispatches EventSaved domain events, and a LoggingSubscriber handles analytics logging at configurable verbosity levels.
Location: pkg/policy/policy.go
Status: The Rule struct is now composed of three focused sub-value objects:
type Rule struct {
Description string
Script string
AccessControl // WriteAllow, WriteDeny, ReadAllow, ReadDeny, etc.
Constraints // SizeLimit, ContentLimit, MaxAgeOfEvent, Privileged, etc.
TagValidationConfig // MustHaveTags, TagValidation, IdentifierRegex
}
Each sub-component handles a specific concern:
AccessControl: Who can read/write (pubkey lists, follows whitelists, permissive flags)Constraints: Event limits and restrictions (size, age, rate, privileged access)TagValidationConfig: Tag presence and format validationJSON serialization remains flat for backward compatibility (embedded structs).
Problem: Some aggregates exposed mutable state via public fields.
Solution Applied:
pkg/acl/acl.go - Fields privatized: acl (was ACL), active (was Active)ACLs(), GetMode(), GetActiveACL(), GetACLByType()app/server.go - Accessor methods exist: GetConfig(), Database(), IsAdmin(), IsOwner()Migration:
// Instead of: acl.Registry.ACL → acl.Registry.ACLs()
// Instead of: acl.Registry.Active.Load() → acl.Registry.GetMode()
Problem: Handlers contained orchestration logic mixed with protocol concerns.
Solution Applied:
app/handle-event.go - Reduced from ~320 lines to ~130 linesingestion.Service.Ingest() for full pipeline orchestrationapp/specialkinds.go for special kind handler registrationProblem: Global acl.Registry singleton made testing difficult.
Solution Applied:
pkg/interfaces/acl/acl.go with Registry interfaceaclRegistry field to Server structACLRegistry() for dependency retrievalmain.go injects acl.Registry during server constructionProblem: Rule struct had 25+ fields, violating single responsibility.
Solution Applied:
pkg/policy/policy.go: - AccessControl: WriteAllow/Deny, ReadAllow/Deny, follows whitelists, permissive flags
- Constraints: SizeLimit, ContentLimit, RateLimit, MaxAgeOfEvent, Privileged, ProtectedRequired
- TagValidationConfig: MustHaveTags, TagValidation, IdentifierRegex
Rule now embeds these three componentsapp/config/config.go)EventRef, IdPkTs)docs/GLOSSARY.md)| File | Purpose |
|---|---|
pkg/domain/errors/errors.go | Typed domain error system |
pkg/domain/events/events.go | Domain event type definitions |
pkg/domain/events/dispatcher.go | Event pub/sub dispatcher |
pkg/domain/events/subscribers/logging.go | Analytics logging subscriber |
| File | Purpose |
|---|---|
pkg/event/validation/validation.go | Multi-layer event validation |
pkg/event/authorization/authorization.go | Authorization service |
pkg/event/routing/routing.go | Kind-based event routing |
pkg/event/processing/processing.go | Event processing orchestration |
pkg/event/ingestion/service.go | Full pipeline ingestion service |
pkg/event/specialkinds/registry.go | Special kind handler registry |
| File | Purpose |
|---|---|
pkg/database/interface.go | Repository interface |
pkg/interfaces/acl/acl.go | ACL interface definition |
pkg/interfaces/store/store_interface.go | Store interfaces, IdPkTs, EventRef |
pkg/policy/policy.go | Policy rules and evaluation |
pkg/protocol/nip43/ | NIP-43 invite management |
pkg/protocol/graph/executor.go | Graph query execution |
| File | Purpose |
|---|---|
app/server.go | HTTP/WebSocket server setup |
app/listener.go | Connection aggregate |
app/handle-event.go | EVENT message handler |
app/handle-*.go | 25+ message type handlers |
| File | Purpose |
|---|---|
pkg/database/database.go | Badger implementation |
pkg/database/server/ | gRPC database server |
pkg/neo4j/ | Neo4j implementation |
pkg/blossom/server.go | Blossom blob storage |
pkg/ratelimit/limiter.go | PID-based rate limiting |
pkg/sync/ | Distributed sync implementations |
| Binary | Purpose |
|---|---|
cmd/orly-launcher/ | Process supervisor with admin UI |
cmd/orly-db-badger/ | Standalone Badger database server |
cmd/orly-acl-follows/ | Follows-based ACL server |
cmd/orly-sync-negentropy/ | NIP-77 negentropy sync service |
| File | Purpose |
|---|---|
docs/GLOSSARY.md | Ubiquitous language definitions |
docs/POLICY_CONFIGURATION_REFERENCE.md | Policy configuration guide |
DDD_ANALYSIS.md | This document |
Generated: 2026-01-24 Analysis based on ORLY codebase v0.56.8