Comprehensive DDD analysis of the Smesh codebase including: - Domain and bounded context identification - Anti-pattern analysis with remediation strategies - 5-phase refactoring roadmap - Migration strategy and success metrics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
787 lines
25 KiB
Markdown
787 lines
25 KiB
Markdown
# Domain-Driven Design Analysis: Smesh Nostr Client
|
|
|
|
## Executive Summary
|
|
|
|
This document provides a Domain-Driven Design (DDD) analysis of the Smesh codebase, a React/TypeScript Nostr client. The analysis identifies the implicit domain model, evaluates current architecture against DDD principles, and provides actionable recommendations for refactoring toward a more domain-centric design.
|
|
|
|
**Key Findings:**
|
|
- The codebase has implicit bounded contexts but lacks explicit boundaries
|
|
- Domain logic is scattered across providers, services, and lib utilities
|
|
- The architecture exhibits several DDD anti-patterns (Anemic Domain Model, Smart UI tendencies)
|
|
- Nostr events naturally align with Domain Events pattern
|
|
- Strong foundation exists for incremental DDD adoption
|
|
|
|
---
|
|
|
|
## 1. Domain Analysis
|
|
|
|
### 1.1 Core Domain Identification
|
|
|
|
The Smesh application operates in the **decentralized social networking** domain, specifically implementing the Nostr protocol. The core business capabilities are:
|
|
|
|
| Subdomain | Type | Description |
|
|
|-----------|------|-------------|
|
|
| **Identity & Authentication** | Core | Key management, signing, account switching |
|
|
| **Social Graph** | Core | Following, muting, trust relationships |
|
|
| **Content Publishing** | Core | Notes, reactions, reposts, media |
|
|
| **Feed Curation** | Core | Timeline construction, filtering, relay selection |
|
|
| **Relay Management** | Supporting | Relay sets, discovery, connectivity |
|
|
| **Notifications** | Supporting | Real-time event monitoring |
|
|
| **Translation** | Generic | Multi-language content translation |
|
|
| **Media Upload** | Generic | NIP-96/Blossom file hosting |
|
|
|
|
### 1.2 Ubiquitous Language
|
|
|
|
The codebase uses Nostr protocol terminology, which forms the basis of the ubiquitous language:
|
|
|
|
| Term | Definition | Current Implementation |
|
|
|------|------------|----------------------|
|
|
| **Event** | Signed JSON object (note, reaction, etc.) | `nostr-tools` Event type |
|
|
| **Pubkey** | User's public key identifier | String (should be Value Object) |
|
|
| **Relay** | WebSocket server for event distribution | String URL (should be Value Object) |
|
|
| **Kind** | Event type identifier (0=profile, 1=note, etc.) | Number constants in `constants.ts` |
|
|
| **Tag** | Metadata attached to events (p, e, a tags) | String arrays (should be Value Objects) |
|
|
| **Profile** | User metadata (name, avatar, etc.) | `TProfile` type |
|
|
| **Follow List** | User's contact list (kind 3) | Array in `FollowListProvider` |
|
|
| **Mute List** | Blocked users/content (kind 10000) | Array in `MuteListProvider` |
|
|
| **Relay List** | User's preferred relays (kind 10002) | `TRelayList` type |
|
|
| **Signer** | Key management abstraction | `ISigner` interface |
|
|
|
|
**Language Issues Identified:**
|
|
- "Stuff Stats" is unclear domain terminology (rename to `InteractionMetrics`)
|
|
- "Favorite Relays" vs "Relay Sets" inconsistency
|
|
- "Draft Event" conflates unsigned events with work-in-progress content
|
|
|
|
---
|
|
|
|
## 2. Current Architecture Assessment
|
|
|
|
### 2.1 Directory Structure Analysis
|
|
|
|
```
|
|
src/
|
|
├── providers/ # State management + some domain logic (17 contexts)
|
|
├── services/ # Business logic + infrastructure concerns mixed
|
|
├── lib/ # Utility functions + domain logic mixed
|
|
├── types/ # Type definitions (implicit domain model)
|
|
├── components/ # UI components (some contain business logic)
|
|
├── pages/ # Page components
|
|
└── hooks/ # Custom React hooks
|
|
```
|
|
|
|
**Assessment:** The architecture follows a layered approach but lacks explicit domain layer separation. Domain logic is distributed across:
|
|
- `lib/` - Event manipulation, validation
|
|
- `services/` - Data fetching, caching, persistence
|
|
- `providers/` - State management with embedded business rules
|
|
|
|
### 2.2 Implicit Bounded Contexts
|
|
|
|
The codebase contains several implicit bounded contexts that could be made explicit:
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────┐
|
|
│ CONTEXT MAP │
|
|
├─────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌──────────────┐ Partnership ┌──────────────┐ │
|
|
│ │ Identity │◄────────────────────►│ Social Graph │ │
|
|
│ │ Context │ │ Context │ │
|
|
│ └──────┬───────┘ └──────┬───────┘ │
|
|
│ │ │ │
|
|
│ │ Customer/Supplier │ │
|
|
│ ▼ ▼ │
|
|
│ ┌──────────────┐ ┌──────────────┐ │
|
|
│ │ Content │ │ Feed │ │
|
|
│ │ Context │ │ Context │ │
|
|
│ └──────────────┘ └──────────────┘ │
|
|
│ │ │ │
|
|
│ └──────────────┬───────────────────────┘ │
|
|
│ ▼ │
|
|
│ ┌──────────────┐ │
|
|
│ │ Relay │ │
|
|
│ │ Context │ │
|
|
│ └──────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
**Context Descriptions:**
|
|
|
|
1. **Identity Context**
|
|
- Concerns: Key management, signing, account switching
|
|
- Current: `NostrProvider`, `ISigner` implementations
|
|
- Entities: Account, Signer
|
|
|
|
2. **Social Graph Context**
|
|
- Concerns: Following, muting, trust, pinned users
|
|
- Current: `FollowListProvider`, `MuteListProvider`, `UserTrustProvider`
|
|
- Entities: User, FollowList, MuteList
|
|
|
|
3. **Content Context**
|
|
- Concerns: Creating and publishing events
|
|
- Current: `lib/draft-event.ts`, publishing logic in providers
|
|
- Entities: Note, Reaction, Repost, Bookmark
|
|
|
|
4. **Feed Context**
|
|
- Concerns: Timeline construction, filtering, display
|
|
- Current: `FeedProvider`, `KindFilterProvider`, `NotificationProvider`
|
|
- Entities: Feed, Filter, Timeline
|
|
|
|
5. **Relay Context**
|
|
- Concerns: Relay management, connectivity, selection
|
|
- Current: `FavoriteRelaysProvider`, `ClientService`
|
|
- Entities: Relay, RelaySet, RelayList
|
|
|
|
---
|
|
|
|
## 3. Anti-Pattern Analysis
|
|
|
|
### 3.1 Anemic Domain Model
|
|
|
|
**Severity: High**
|
|
|
|
The current implementation exhibits a classic anemic domain model. Domain types are primarily data structures without behavior.
|
|
|
|
**Evidence:**
|
|
|
|
```typescript
|
|
// Current: Types are data containers (src/types/index.d.ts)
|
|
type TProfile = {
|
|
pubkey: string
|
|
username?: string
|
|
displayName?: string
|
|
avatar?: string
|
|
// ... no behavior
|
|
}
|
|
|
|
// Business logic lives in external functions (src/lib/event-metadata.ts)
|
|
export function extractProfileFromEventContent(event: Event): TProfile {
|
|
// Logic external to the domain object
|
|
}
|
|
```
|
|
|
|
**Impact:**
|
|
- Business rules scattered across `lib/`, `services/`, `providers/`
|
|
- Difficult to find all rules related to a concept
|
|
- Easy to bypass validation by directly manipulating data
|
|
|
|
### 3.2 Smart UI Tendencies
|
|
|
|
**Severity: Medium**
|
|
|
|
Some business logic exists in UI components and providers that should be in domain layer.
|
|
|
|
**Evidence:**
|
|
|
|
```typescript
|
|
// Provider contains domain logic (src/providers/FollowListProvider.tsx)
|
|
const follow = async (pubkey: string) => {
|
|
// Business rule: can't follow yourself
|
|
if (pubkey === currentPubkey) return
|
|
|
|
// Business rule: avoid duplicates
|
|
if (followList.includes(pubkey)) return
|
|
|
|
// Event creation and publishing
|
|
const newFollowList = [...followList, pubkey]
|
|
const draftEvent = createFollowListDraftEvent(...)
|
|
await publish(draftEvent)
|
|
}
|
|
```
|
|
|
|
This logic belongs in a domain service or aggregate, not in a React context provider.
|
|
|
|
### 3.3 Database-Driven Design Elements
|
|
|
|
**Severity: Low**
|
|
|
|
The `IndexedDB` schema influences some type definitions, though this is less severe than traditional database-driven design.
|
|
|
|
**Evidence:**
|
|
- Storage keys defined alongside domain constants
|
|
- Some types mirror storage structure rather than domain concepts
|
|
|
|
### 3.4 Missing Aggregate Boundaries
|
|
|
|
**Severity: Medium**
|
|
|
|
No explicit aggregate roots or boundaries exist. Related data is managed independently.
|
|
|
|
**Evidence:**
|
|
- `FollowList`, `MuteList`, `PinList` are managed by separate providers
|
|
- No transactional consistency guarantees
|
|
- Cross-cutting updates happen independently
|
|
|
|
### 3.5 Leaky Abstractions
|
|
|
|
**Severity: Medium**
|
|
|
|
Infrastructure concerns leak into what should be domain logic.
|
|
|
|
**Evidence:**
|
|
|
|
```typescript
|
|
// Service mixes domain and infrastructure (src/services/client.service.ts)
|
|
class ClientService extends EventTarget {
|
|
private pool = new SimplePool() // Infrastructure
|
|
private cache = new LRUCache(...) // Infrastructure
|
|
private userIndex = new FlexSearch(...) // Infrastructure
|
|
|
|
// Domain logic mixed with caching, batching, retries
|
|
async fetchProfile(pubkey: string): Promise<TProfile | null> {
|
|
// Caching logic
|
|
// Relay selection logic (domain)
|
|
// Network calls (infrastructure)
|
|
// Index updates (infrastructure)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Current Strengths
|
|
|
|
### 4.1 Natural Domain Event Alignment
|
|
|
|
Nostr events ARE domain events. The protocol's event-sourced nature aligns perfectly with DDD:
|
|
|
|
```typescript
|
|
// Nostr events capture domain facts
|
|
{
|
|
kind: 1, // Note created
|
|
content: "Hello Nostr!",
|
|
tags: [["p", "..."]], // Mentions
|
|
created_at: 1234567890,
|
|
pubkey: "...",
|
|
sig: "..."
|
|
}
|
|
```
|
|
|
|
### 4.2 Signer Interface Abstraction
|
|
|
|
The `ISigner` interface is a well-designed port in hexagonal architecture terms:
|
|
|
|
```typescript
|
|
interface ISigner {
|
|
getPublicKey(): Promise<string>
|
|
signEvent(draftEvent: TDraftEvent): Promise<VerifiedEvent>
|
|
nip04Encrypt(pubkey: string, plainText: string): Promise<string>
|
|
nip04Decrypt(pubkey: string, cipherText: string): Promise<string>
|
|
}
|
|
```
|
|
|
|
Multiple implementations exist: `NsecSigner`, `Nip07Signer`, `BunkerSigner`, etc.
|
|
|
|
### 4.3 Event Creation Factories
|
|
|
|
The `lib/draft-event.ts` file contains factory functions that encapsulate event creation:
|
|
|
|
```typescript
|
|
createShortTextNoteDraftEvent(content, tags?, relays?)
|
|
createReactionDraftEvent(event, emoji?)
|
|
createFollowListDraftEvent(tags, content?)
|
|
createBookmarkDraftEvent(tags, content?)
|
|
```
|
|
|
|
These are proto-factories that could be formalized into proper Factory patterns.
|
|
|
|
### 4.4 Clear Type Definitions
|
|
|
|
The `types/index.d.ts` file provides a foundation for a rich domain model, even if currently anemic.
|
|
|
|
---
|
|
|
|
## 5. Refactoring Recommendations
|
|
|
|
### 5.1 Phase 1: Establish Domain Layer (Low Risk)
|
|
|
|
**Goal:** Create explicit domain layer without disrupting existing functionality.
|
|
|
|
**Actions:**
|
|
|
|
1. **Create domain directory structure:**
|
|
|
|
```
|
|
src/
|
|
├── domain/
|
|
│ ├── identity/
|
|
│ │ ├── Account.ts
|
|
│ │ ├── Pubkey.ts (Value Object)
|
|
│ │ └── index.ts
|
|
│ ├── social/
|
|
│ │ ├── FollowList.ts (Aggregate)
|
|
│ │ ├── MuteList.ts (Aggregate)
|
|
│ │ └── index.ts
|
|
│ ├── content/
|
|
│ │ ├── Note.ts (Entity)
|
|
│ │ ├── Reaction.ts (Value Object)
|
|
│ │ └── index.ts
|
|
│ ├── relay/
|
|
│ │ ├── Relay.ts (Value Object)
|
|
│ │ ├── RelaySet.ts (Aggregate)
|
|
│ │ └── index.ts
|
|
│ └── shared/
|
|
│ ├── EventId.ts
|
|
│ ├── Timestamp.ts
|
|
│ └── index.ts
|
|
```
|
|
|
|
2. **Introduce Value Objects for primitives:**
|
|
|
|
```typescript
|
|
// src/domain/identity/Pubkey.ts
|
|
export class Pubkey {
|
|
private constructor(private readonly value: string) {}
|
|
|
|
static fromHex(hex: string): Pubkey {
|
|
if (!/^[0-9a-f]{64}$/i.test(hex)) {
|
|
throw new InvalidPubkeyError(hex)
|
|
}
|
|
return new Pubkey(hex)
|
|
}
|
|
|
|
static fromNpub(npub: string): Pubkey {
|
|
const decoded = nip19.decode(npub)
|
|
if (decoded.type !== 'npub') {
|
|
throw new InvalidPubkeyError(npub)
|
|
}
|
|
return new Pubkey(decoded.data)
|
|
}
|
|
|
|
toHex(): string { return this.value }
|
|
toNpub(): string { return nip19.npubEncode(this.value) }
|
|
|
|
equals(other: Pubkey): boolean {
|
|
return this.value === other.value
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// src/domain/relay/RelayUrl.ts
|
|
export class RelayUrl {
|
|
private constructor(private readonly value: string) {}
|
|
|
|
static create(url: string): RelayUrl {
|
|
const normalized = normalizeRelayUrl(url)
|
|
if (!isValidRelayUrl(normalized)) {
|
|
throw new InvalidRelayUrlError(url)
|
|
}
|
|
return new RelayUrl(normalized)
|
|
}
|
|
|
|
toString(): string { return this.value }
|
|
|
|
equals(other: RelayUrl): boolean {
|
|
return this.value === other.value
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Create rich domain entities:**
|
|
|
|
```typescript
|
|
// src/domain/social/FollowList.ts
|
|
export class FollowList {
|
|
private constructor(
|
|
private readonly _ownerPubkey: Pubkey,
|
|
private _following: Set<string>,
|
|
private _petnames: Map<string, string>
|
|
) {}
|
|
|
|
static empty(owner: Pubkey): FollowList {
|
|
return new FollowList(owner, new Set(), new Map())
|
|
}
|
|
|
|
static fromEvent(event: Event): FollowList {
|
|
// Reconstitute from Nostr event
|
|
}
|
|
|
|
follow(pubkey: Pubkey): FollowListUpdated {
|
|
if (pubkey.equals(this._ownerPubkey)) {
|
|
throw new CannotFollowSelfError()
|
|
}
|
|
if (this._following.has(pubkey.toHex())) {
|
|
return FollowListUpdated.noChange()
|
|
}
|
|
this._following.add(pubkey.toHex())
|
|
return FollowListUpdated.added(pubkey)
|
|
}
|
|
|
|
unfollow(pubkey: Pubkey): FollowListUpdated {
|
|
if (!this._following.has(pubkey.toHex())) {
|
|
return FollowListUpdated.noChange()
|
|
}
|
|
this._following.delete(pubkey.toHex())
|
|
return FollowListUpdated.removed(pubkey)
|
|
}
|
|
|
|
isFollowing(pubkey: Pubkey): boolean {
|
|
return this._following.has(pubkey.toHex())
|
|
}
|
|
|
|
toDraftEvent(): TDraftEvent {
|
|
// Convert to publishable event
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.2 Phase 2: Introduce Domain Services (Medium Risk)
|
|
|
|
**Goal:** Extract business logic from providers into domain services.
|
|
|
|
**Actions:**
|
|
|
|
1. **Create domain services for cross-aggregate operations:**
|
|
|
|
```typescript
|
|
// src/domain/content/PublishingService.ts
|
|
export class PublishingService {
|
|
constructor(
|
|
private readonly relaySelector: RelaySelector,
|
|
private readonly signer: ISigner
|
|
) {}
|
|
|
|
async publishNote(
|
|
content: string,
|
|
mentions: Pubkey[],
|
|
replyTo?: EventId
|
|
): Promise<PublishedNote> {
|
|
const note = Note.create(content, mentions, replyTo)
|
|
const relays = await this.relaySelector.selectForPublishing(note)
|
|
const signedEvent = await this.signer.signEvent(note.toDraftEvent())
|
|
|
|
return new PublishedNote(signedEvent, relays)
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript
|
|
// src/domain/relay/RelaySelector.ts
|
|
export class RelaySelector {
|
|
constructor(
|
|
private readonly userRelayList: RelayList,
|
|
private readonly mentionRelayResolver: MentionRelayResolver
|
|
) {}
|
|
|
|
async selectForPublishing(note: Note): Promise<RelayUrl[]> {
|
|
const writeRelays = this.userRelayList.writeRelays()
|
|
const mentionRelays = await this.resolveMentionRelays(note.mentions)
|
|
|
|
return this.mergeAndDeduplicate(writeRelays, mentionRelays)
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Refactor providers to use domain services:**
|
|
|
|
```typescript
|
|
// src/providers/ContentProvider.tsx (refactored)
|
|
export function ContentProvider({ children }: Props) {
|
|
const { signer, relayList } = useNostr()
|
|
|
|
// Domain service instantiation
|
|
const publishingService = useMemo(
|
|
() => new PublishingService(
|
|
new RelaySelector(relayList, new MentionRelayResolver()),
|
|
signer
|
|
),
|
|
[signer, relayList]
|
|
)
|
|
|
|
const publishNote = useCallback(async (content: string, mentions: string[]) => {
|
|
const pubkeys = mentions.map(Pubkey.fromHex)
|
|
const result = await publishingService.publishNote(content, pubkeys)
|
|
// Update UI state
|
|
}, [publishingService])
|
|
|
|
return (
|
|
<ContentContext.Provider value={{ publishNote }}>
|
|
{children}
|
|
</ContentContext.Provider>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 5.3 Phase 3: Define Aggregate Boundaries (Higher Risk)
|
|
|
|
**Goal:** Establish clear aggregate roots with transactional boundaries.
|
|
|
|
**Proposed Aggregates:**
|
|
|
|
| Aggregate Root | Child Entities | Invariants |
|
|
|----------------|----------------|------------|
|
|
| `UserProfile` | Profile metadata | NIP-05 validation |
|
|
| `FollowList` | Follow entries, petnames | No self-follow, unique entries |
|
|
| `MuteList` | Public mutes, private mutes | Encryption for private |
|
|
| `RelaySet` | Relay URLs, names | Valid URLs, unique within set |
|
|
| `Bookmark` | Bookmarked events | Unique event references |
|
|
|
|
**Implementation:**
|
|
|
|
```typescript
|
|
// src/domain/social/FollowList.ts (Aggregate Root)
|
|
export class FollowList {
|
|
private _domainEvents: DomainEvent[] = []
|
|
|
|
follow(pubkey: Pubkey): void {
|
|
// Invariant enforcement
|
|
this.ensureNotSelf(pubkey)
|
|
this.ensureNotAlreadyFollowing(pubkey)
|
|
|
|
this._following.add(pubkey.toHex())
|
|
|
|
// Raise domain event
|
|
this._domainEvents.push(
|
|
new UserFollowed(this._ownerPubkey, pubkey, new Date())
|
|
)
|
|
}
|
|
|
|
pullDomainEvents(): DomainEvent[] {
|
|
const events = [...this._domainEvents]
|
|
this._domainEvents = []
|
|
return events
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.4 Phase 4: Introduce Repositories (Higher Risk)
|
|
|
|
**Goal:** Abstract persistence behind domain-focused interfaces.
|
|
|
|
```typescript
|
|
// src/domain/social/FollowListRepository.ts (Interface in domain)
|
|
export interface FollowListRepository {
|
|
findByOwner(pubkey: Pubkey): Promise<FollowList | null>
|
|
save(followList: FollowList): Promise<void>
|
|
}
|
|
|
|
// src/infrastructure/persistence/IndexedDbFollowListRepository.ts
|
|
export class IndexedDbFollowListRepository implements FollowListRepository {
|
|
constructor(
|
|
private readonly indexedDb: IndexedDbService,
|
|
private readonly clientService: ClientService
|
|
) {}
|
|
|
|
async findByOwner(pubkey: Pubkey): Promise<FollowList | null> {
|
|
// Check IndexedDB cache
|
|
const cached = await this.indexedDb.getFollowList(pubkey.toHex())
|
|
if (cached) {
|
|
return FollowList.fromEvent(cached)
|
|
}
|
|
|
|
// Fetch from relays
|
|
const event = await this.clientService.fetchFollowList(pubkey.toHex())
|
|
if (event) {
|
|
await this.indexedDb.saveFollowList(event)
|
|
return FollowList.fromEvent(event)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
async save(followList: FollowList): Promise<void> {
|
|
const draftEvent = followList.toDraftEvent()
|
|
// Sign and publish handled by application service
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5.5 Phase 5: Event-Driven Architecture (Advanced)
|
|
|
|
**Goal:** Leverage Nostr's event-sourced nature for cross-context communication.
|
|
|
|
```typescript
|
|
// src/domain/shared/DomainEvent.ts
|
|
export abstract class DomainEvent {
|
|
readonly occurredAt: Date = new Date()
|
|
abstract get eventType(): string
|
|
}
|
|
|
|
// src/domain/social/events/UserFollowed.ts
|
|
export class UserFollowed extends DomainEvent {
|
|
constructor(
|
|
readonly follower: Pubkey,
|
|
readonly followed: Pubkey,
|
|
readonly timestamp: Date
|
|
) {
|
|
super()
|
|
}
|
|
|
|
get eventType(): string { return 'social.user_followed' }
|
|
}
|
|
|
|
// src/application/handlers/UserFollowedHandler.ts
|
|
export class UserFollowedHandler {
|
|
constructor(
|
|
private readonly notificationService: NotificationService
|
|
) {}
|
|
|
|
async handle(event: UserFollowed): Promise<void> {
|
|
// Cross-context reaction
|
|
await this.notificationService.notifyNewFollower(
|
|
event.followed,
|
|
event.follower
|
|
)
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. Proposed Target Architecture
|
|
|
|
```
|
|
src/
|
|
├── domain/ # Core domain logic (no dependencies)
|
|
│ ├── identity/
|
|
│ │ ├── model/
|
|
│ │ │ ├── Account.ts
|
|
│ │ │ ├── Pubkey.ts
|
|
│ │ │ └── Keypair.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── SigningService.ts
|
|
│ │ └── index.ts
|
|
│ ├── social/
|
|
│ │ ├── model/
|
|
│ │ │ ├── FollowList.ts
|
|
│ │ │ ├── MuteList.ts
|
|
│ │ │ └── UserProfile.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── TrustCalculator.ts
|
|
│ │ ├── events/
|
|
│ │ │ ├── UserFollowed.ts
|
|
│ │ │ └── UserMuted.ts
|
|
│ │ └── index.ts
|
|
│ ├── content/
|
|
│ │ ├── model/
|
|
│ │ │ ├── Note.ts
|
|
│ │ │ ├── Reaction.ts
|
|
│ │ │ └── Repost.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── ContentValidator.ts
|
|
│ │ └── index.ts
|
|
│ ├── relay/
|
|
│ │ ├── model/
|
|
│ │ │ ├── RelayUrl.ts
|
|
│ │ │ ├── RelaySet.ts
|
|
│ │ │ └── RelayList.ts
|
|
│ │ ├── services/
|
|
│ │ │ └── RelaySelector.ts
|
|
│ │ └── index.ts
|
|
│ └── shared/
|
|
│ ├── EventId.ts
|
|
│ ├── Timestamp.ts
|
|
│ └── DomainEvent.ts
|
|
│
|
|
├── application/ # Use cases, orchestration
|
|
│ ├── identity/
|
|
│ │ └── AccountService.ts
|
|
│ ├── social/
|
|
│ │ ├── FollowService.ts
|
|
│ │ └── MuteService.ts
|
|
│ ├── content/
|
|
│ │ └── PublishingService.ts
|
|
│ └── handlers/
|
|
│ └── DomainEventHandlers.ts
|
|
│
|
|
├── infrastructure/ # External concerns
|
|
│ ├── persistence/
|
|
│ │ ├── IndexedDbRepository.ts
|
|
│ │ └── LocalStorageRepository.ts
|
|
│ ├── nostr/
|
|
│ │ ├── NostrClient.ts
|
|
│ │ └── RelayPool.ts
|
|
│ ├── signing/
|
|
│ │ ├── NsecSigner.ts
|
|
│ │ ├── Nip07Signer.ts
|
|
│ │ └── BunkerSigner.ts
|
|
│ └── translation/
|
|
│ └── TranslationApiClient.ts
|
|
│
|
|
├── presentation/ # React components
|
|
│ ├── providers/ # Thin wrappers around application services
|
|
│ ├── components/
|
|
│ ├── pages/
|
|
│ └── hooks/
|
|
│
|
|
└── shared/ # Cross-cutting utilities
|
|
├── lib/
|
|
└── constants/
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Migration Strategy
|
|
|
|
### 7.1 Incremental Approach
|
|
|
|
1. **Week 1-2:** Create `domain/shared/` with Value Objects (Pubkey, RelayUrl, EventId)
|
|
2. **Week 3-4:** Migrate one bounded context (recommend: Social Graph)
|
|
3. **Week 5-6:** Add domain services, refactor related providers
|
|
4. **Week 7-8:** Introduce repositories for the migrated context
|
|
5. **Ongoing:** Repeat for remaining contexts
|
|
|
|
### 7.2 Coexistence Strategy
|
|
|
|
During migration, old and new code can coexist:
|
|
|
|
```typescript
|
|
// Adapter to bridge old and new
|
|
export function legacyPubkeyToDomain(pubkey: string): Pubkey {
|
|
return Pubkey.fromHex(pubkey)
|
|
}
|
|
|
|
export function domainPubkeyToLegacy(pubkey: Pubkey): string {
|
|
return pubkey.toHex()
|
|
}
|
|
```
|
|
|
|
### 7.3 Testing Strategy
|
|
|
|
- Unit test domain objects in isolation
|
|
- Integration test application services
|
|
- Keep existing component tests as regression safety
|
|
|
|
---
|
|
|
|
## 8. Metrics for Success
|
|
|
|
| Metric | Current State | Target State |
|
|
|--------|---------------|--------------|
|
|
| Domain logic in providers | ~60% | <10% |
|
|
| Value Objects usage | 0 | 15+ types |
|
|
| Explicit aggregates | 0 | 5 aggregates |
|
|
| Domain events | 0 (implicit) | 10+ event types |
|
|
| Repository interfaces | 0 | 5 repositories |
|
|
| Test coverage (domain) | N/A | >80% |
|
|
|
|
---
|
|
|
|
## 9. Risks and Mitigations
|
|
|
|
| Risk | Probability | Impact | Mitigation |
|
|
|------|-------------|--------|------------|
|
|
| Breaking changes during migration | Medium | High | Incremental migration, adapter layer |
|
|
| Performance regression | Low | Medium | Benchmark critical paths, optimize lazily |
|
|
| Team learning curve | Medium | Medium | Documentation, pair programming |
|
|
| Over-engineering | Medium | Medium | YAGNI principle, concrete before abstract |
|
|
|
|
---
|
|
|
|
## 10. Conclusion
|
|
|
|
The Smesh codebase has a solid foundation that can be evolved toward DDD principles. The key recommendations are:
|
|
|
|
1. **Immediate:** Introduce Value Objects for Pubkey, RelayUrl, EventId
|
|
2. **Short-term:** Create rich domain entities with behavior
|
|
3. **Medium-term:** Extract domain services from providers
|
|
4. **Long-term:** Full bounded context separation with repositories
|
|
|
|
The Nostr protocol's event-sourced nature is a natural fit for DDD, and the existing type definitions provide a starting point for a rich domain model. The main effort will be moving from an anemic model to entities with behavior, and establishing clear aggregate boundaries.
|
|
|
|
---
|
|
|
|
*Generated: December 2024*
|
|
*Analysis based on DDD principles from Eric Evans and Vaughn Vernon*
|