DDD_ANALYSIS.md raw

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.

Analysis Version: v0.56.8 Last Updated: 2026-01-24

Key Recommendations Summary

#RecommendationImpactEffortStatus
1Domain Events & DispatcherHighMediumImplemented
2Domain-Specific Error TypesMediumLowImplemented
3Application Service ExtractionMediumHighImplemented
4Value Object ImmutabilityLowLowImplemented
5Ubiquitous Language GlossaryMediumLowImplemented
6Aggregate Boundary StrengtheningHighMediumImplemented
7Document Context MapMediumLowThis Document
8Handler SimplificationMediumMediumImplemented
9Injectable ACL RegistryMediumLowImplemented
10Rule Value Object DecompositionLowMediumImplemented

Table of Contents

  1. Executive Summary
  2. Strategic Design Analysis

- Bounded Contexts - Context Map - Subdomain Classification

  1. Tactical Design Analysis

- Entities - Value Objects - Aggregates - Repositories - Domain Services - Domain Events - Error Handling

  1. Anti-Patterns Identified
  2. Remaining Recommendations
  3. Implementation Checklist
  4. Appendix: File References

Executive Summary

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.

DDD Maturity Score: 10/10

Recent Improvements (v0.56.5-v0.56.8):

Strengths:

Strategic Design Analysis

Bounded Contexts

ORLY organizes code into 11 distinct bounded contexts, each with its own model and language:

1. Event Storage Context (pkg/database/)

2. Access Control Context (pkg/acl/)

3. Event Policy Context (pkg/policy/)

4. Event Processing Context (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

5. Connection Management Context (app/)

6. Domain Layer (pkg/domain/) NEW

- events.DomainEvent - Base event interface - events.Dispatcher - Pub/sub with sync/async publishing - errors.DomainError - Typed error categories

7. Protocol Extensions Context (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

8. Blob Storage Context (pkg/blossom/)

9. Rate Limiting Context (pkg/ratelimit/)

10. Distributed Sync Context (pkg/sync/)

11. Process Supervision Context (cmd/orly-launcher/)

Context Map

┌─────────────────────────────────────────────────────────────────────────────────┐
│                      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:

UpstreamDownstreamPatternNotes
nostr libraryAll contextsShared KernelEvent, Filter, Tag types
DatabaseACL, Policy, Blossom, SyncCustomer-SupplierQuery for follows, permissions
PolicyHandlers, SyncConformistAll respect policy decisions
Domain EventsProcessing, ACLPub/SubEvent dispatcher decouples contexts
gRPC LayerDB, ACL, SyncPublished LanguageProtocol buffer contracts

Subdomain Classification

SubdomainTypeJustification
Event StorageCoreCentral to relay's value proposition
Access ControlCoreKey differentiator (WoT, follows-based, managed, curating)
Event PolicyCoreEnables complex filtering rules
Event ProcessingCoreClean orchestration of event pipeline
Graph QueriesCoreUnique social graph traversal capabilities
NIP-43 MembershipCoreUnique invite-based access model
Blob Storage (Blossom)CoreMedia hosting differentiator
Domain EventsSupportingCross-cutting concern infrastructure
Connection ManagementSupportingStandard WebSocket infrastructure
Rate LimitingSupportingOperational concern with PID controller
Process SupervisionGenericStandard process management

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
    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
}

InviteCode (NIP-43 Entity)

// pkg/protocol/nip43/
type InviteCode struct {
    Code      string      // Identity: unique code
    ExpiresAt time.Time
    UsedBy    []byte      // Tracks consumption
    CreatedAt time.Time
}

Process (Supervisor Entity)

// 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

Value objects are immutable and defined by their attributes, not identity.

EventRef (Immutable Event Reference) - Cache-Line Optimized

// 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

IdPkTs (Stack-Allocated Event Reference) UPDATED v0.56.6

// 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[:] }

Decision (Authorization Result)

// 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
}

Kinds (Policy Specification)

// pkg/policy/policy.go:58-63
type Kinds struct {
    Whitelist []int
    Blacklist []int
}

Rule Sub-Components (UPDATED v0.56.8)

// 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

Aggregates are clusters of entities/value objects with consistency boundaries.

Listener Aggregate

- Subscriptions must exist before receiving matching events - AUTH must complete before authenticated operations - Message processing uses RWMutex for pause/resume during policy updates

Event Aggregate (External)

- ID must match computed hash - Signature must be valid - Timestamp must be within bounds

Supervisor Aggregate

- Dependency ordering (DB -> ACL -> Sync -> Relay) - Health checks before dependent startup - Reverse shutdown ordering

Repositories

The Repository pattern abstracts persistence for aggregate roots.

Database Interface (Primary Repository)

// 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:

BackendTypeUse CaseLocation
BadgerLSM TreePrimary - high throughputpkg/database/
Neo4jProperty GraphSocial queriespkg/neo4j/
WasmDBIndexedDBBrowser WASMpkg/wasmdb/
gRPCRemote serviceSplit IPC modepkg/database/grpc/

Driver Registry Pattern

// Runtime driver selection
database.HasDriver("badger")
database.NewFromDriver("badger", config)

acl.HasDriver("follows")
acl.NewACLFromDriver("follows", config)

Domain Services

Domain services encapsulate logic that doesn't belong to any single entity.

Event Validation Service NEW

// 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

Event Authorization Service NEW

// 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

Event Router Service NEW

// 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)
}

Event Processing Service NEW

// 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

Event Ingestion Service NEW

// 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

ACL Registry (Access Decision Service)

// pkg/acl/acl.go
func (s *S) GetAccessLevel(pub []byte, address string) string
func (s *S) CheckPolicy(ev *event.E) (bool, error)

Policy Manager (Rule Evaluation Service)

// pkg/policy/policy.go
func (p *P) CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error)

Domain Events

Status: IMPLEMENTED (v0.56.5)

Domain Event Interface

// pkg/domain/events/events.go
type DomainEvent interface {
    OccurredAt() time.Time
    EventType() string
}

type Base struct {
    occurredAt time.Time
    eventType  string
}

Concrete Event Types

// 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...

Event Dispatcher

// 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)

Error Handling

Status: IMPLEMENTED (v0.56.5)

Typed Domain Errors

// 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 }

Error Helpers

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

Anti-Patterns Identified

1. ~~Remaining Handler Complexity~~ (RESOLVED v0.56.8)

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.

2. ~~Global Singleton Registry~~ (RESOLVED v0.56.8)

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.

3. ~~Partial Event Dispatcher Usage~~ (RESOLVED v0.56.7)

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.

4. ~~Oversized Rule Value Object~~ (RESOLVED v0.56.8)

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:

JSON serialization remains flat for backward compatibility (embedded structs).


Completed Recommendations

6. ~~Aggregate Boundary Strengthening~~ (RESOLVED v0.56.7)

Problem: Some aggregates exposed mutable state via public fields.

Solution Applied:

Migration:

// Instead of: acl.Registry.ACL → acl.Registry.ACLs()
// Instead of: acl.Registry.Active.Load() → acl.Registry.GetMode()

8. Handler Simplification (RESOLVED v0.56.8)

Problem: Handlers contained orchestration logic mixed with protocol concerns.

Solution Applied:

9. Injectable ACL Registry (RESOLVED v0.56.8)

Problem: Global acl.Registry singleton made testing difficult.

Solution Applied:

10. Rule Value Object Decomposition (RESOLVED v0.56.8)

Problem: Rule struct had 25+ fields, violating single responsibility.

Solution Applied:

- AccessControl: WriteAllow/Deny, ReadAllow/Deny, follows whitelists, permissive flags - Constraints: SizeLimit, ContentLimit, RateLimit, MaxAgeOfEvent, Privileged, ProtectedRequired - TagValidationConfig: MustHaveTags, TagValidation, IdentifierRegex

Implementation Checklist

All Items Complete ✓

Appendix: File References

Domain Layer Files (NEW)

FilePurpose
pkg/domain/errors/errors.goTyped domain error system
pkg/domain/events/events.goDomain event type definitions
pkg/domain/events/dispatcher.goEvent pub/sub dispatcher
pkg/domain/events/subscribers/logging.goAnalytics logging subscriber

Event Processing Layer (NEW)

FilePurpose
pkg/event/validation/validation.goMulti-layer event validation
pkg/event/authorization/authorization.goAuthorization service
pkg/event/routing/routing.goKind-based event routing
pkg/event/processing/processing.goEvent processing orchestration
pkg/event/ingestion/service.goFull pipeline ingestion service
pkg/event/specialkinds/registry.goSpecial kind handler registry

Core Domain Files

FilePurpose
pkg/database/interface.goRepository interface
pkg/interfaces/acl/acl.goACL interface definition
pkg/interfaces/store/store_interface.goStore interfaces, IdPkTs, EventRef
pkg/policy/policy.goPolicy rules and evaluation
pkg/protocol/nip43/NIP-43 invite management
pkg/protocol/graph/executor.goGraph query execution

Application Layer Files

FilePurpose
app/server.goHTTP/WebSocket server setup
app/listener.goConnection aggregate
app/handle-event.goEVENT message handler
app/handle-*.go25+ message type handlers

Infrastructure Files

FilePurpose
pkg/database/database.goBadger implementation
pkg/database/server/gRPC database server
pkg/neo4j/Neo4j implementation
pkg/blossom/server.goBlossom blob storage
pkg/ratelimit/limiter.goPID-based rate limiting
pkg/sync/Distributed sync implementations

Command Binaries

BinaryPurpose
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

Documentation

FilePurpose
docs/GLOSSARY.mdUbiquitous language definitions
docs/POLICY_CONFIGURATION_REFERENCE.mdPolicy configuration guide
DDD_ANALYSIS.mdThis document

Generated: 2026-01-24 Analysis based on ORLY codebase v0.56.8