bitpill 0.3.3

A personal medication management TUI application built in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# Domain-Driven Design in Rust

A practical guide to the core DDD building blocks and how they map to Rust's type system,
ownership model, and module structure. Examples are drawn from the BitPill domain wherever
possible.

---

## Table of Contents

1. [Value Object]#1-value-object
2. [Entity]#2-entity
3. [Aggregate Root]#3-aggregate-root
4. [Domain Event]#4-domain-event
5. [Repository]#5-repository
6. [Domain Service]#6-domain-service
7. [Factory]#7-factory
8. [Bounded Context]#8-bounded-context
9. [Ubiquitous Language]#9-ubiquitous-language
10. [Summary]#10-summary

---

## 1. Value Object

> An object that has no conceptual identity. Two value objects are equal if all their
> attributes are equal.

Value objects are the cheapest and most powerful modelling tool in DDD. They replace
primitive types (`String`, `u32`, `i64`) with domain-meaningful types that self-validate.

### Characteristics

- **Immutable** — no `&mut self` methods.
- **Equality by value** — derive `PartialEq`, `Eq`.
- **Self-validating** — invariants enforced in `fn new(...) -> Result<Self, DomainError>`.
- **No identity** — two instances with identical data are interchangeable.

### Example — `Dosage`

```rust
// src/domain/value_objects/dosage.rs

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Dosage {
    amount_mg: u32,   // private: callers cannot construct an invalid Dosage
}

impl Dosage {
    pub fn new(amount_mg: u32) -> Result<Self, DomainError> {
        if amount_mg == 0 {
            return Err(DomainError::InvalidDosage);
        }
        Ok(Self { amount_mg })
    }

    pub fn amount_mg(&self) -> u32 {
        self.amount_mg
    }
}

impl std::fmt::Display for Dosage {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}mg", self.amount_mg)
    }
}
```

`Dosage` cannot be created with `0` mg. Any code that holds a `Dosage` can trust it is
valid — the constructor is the only guard needed.

### Why not just `u32`?

```rust
// ❌ Primitive obsession — what unit? Is 0 valid? Is 10_000 valid?
fn prescribe(amount: u32) { /* ... */ }

// ✅ Self-documenting, self-validating
fn prescribe(dosage: Dosage) { /* ... */ }
```

### Value object checklist

- Fields are private; expose read-only accessors only.
- Derive `Clone`, `PartialEq`, `Eq` (and `Hash` when used in maps/sets).
- Never expose `&mut self` methods.
- All validation in `new` — never in the caller.

---

## 2. Entity

> An object defined by its identity, not its attributes. Two entities with the same `id`
> are the same object even if all other fields differ.

### Characteristics

- Has an **`id` field** (typically a newtype wrapping `Uuid`).
- **Mutable state** — entities transition through a lifecycle.
- **Identity equality** — two entities are the same iff their `id` matches.
- **Carries behaviour** — not an anemic bag of data.

### Example — `DoseRecord`

```rust
// src/domain/entities/dose_record.rs

pub struct DoseRecord {
    id: DoseRecordId,
    medication_id: MedicationId,
    scheduled_at: NaiveDateTime,
    taken_at: Option<NaiveDateTime>,   // ← mutable lifecycle state
}

impl DoseRecord {
    pub fn new(medication_id: MedicationId, scheduled_at: NaiveDateTime) -> Self {
        Self {
            id: DoseRecordId::generate(),
            medication_id,
            scheduled_at,
            taken_at: None,
        }
    }

    /// State transition: pending → taken. Enforces the "only once" invariant.
    pub fn mark_taken(&mut self, at: NaiveDateTime) -> Result<(), DomainError> {
        if self.taken_at.is_some() {
            return Err(DomainError::DoseAlreadyTaken);
        }
        self.taken_at = Some(at);
        Ok(())
    }

    pub fn is_taken(&self) -> bool {
        self.taken_at.is_some()
    }
}
```

`mark_taken` is not a setter — it is a **domain command** that enforces the business rule
"a dose can only be marked taken once."

### Identity newtype

```rust
// src/domain/value_objects/dose_record_id.rs

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DoseRecordId(Uuid);

impl DoseRecordId {
    pub fn generate() -> Self {
        Self(Uuid::now_v7())
    }
}
```

Wrapping `Uuid` in a newtype prevents accidentally passing a `MedicationId` where a
`DoseRecordId` is expected — the compiler enforces it.

---

## 3. Aggregate Root

> A cluster of domain objects treated as a single unit for the purpose of data changes.
> The root is the only entry point — external code never holds direct references to
> inner objects.

### Characteristics

- **Single entry point** — all mutations go through the root.
- **Owns its boundaries** — child entities and value objects are private or exposed
  read-only.
- **Consistency boundary** — a repository loads and saves the entire aggregate atomically.
- **Guards invariants across the cluster**.

### Example — `Medication` as Aggregate Root

```rust
// src/domain/entities/medication.rs

pub struct Medication {
    id: MedicationId,
    name: MedicationName,
    dosage: Dosage,
    scheduled_time: Vec<ScheduledTime>,   // child value objects
}

impl Medication {
    pub fn new(
        id: MedicationId,
        name: MedicationName,
        dosage: Dosage,
        scheduled_time: Vec<ScheduledTime>,
    ) -> Self { /* ... */ }

    // Read-only access to children — callers cannot mutate them directly
    pub fn scheduled_time(&self) -> &[ScheduledTime] {
        &self.scheduled_time
    }

    // All mutations go through the root
    pub fn reschedule(&mut self, times: Vec<ScheduledTime>) {
        self.scheduled_time = times;
    }

    pub fn update_dosage(&mut self, dosage: Dosage) {
        self.dosage = dosage;
    }
}
```

### Aggregate boundary rules

```rust
// ❌ Bypass the root — breaks the consistency boundary
let times = &mut medication.scheduled_time; // field is private, compiler rejects this

// ✅ All changes via the root
medication.reschedule(vec![ScheduledTime::new(9, 0).unwrap()]);
```

### References between aggregates — IDs only

```rust
// ❌ Holding a reference to another aggregate breaks the boundary
pub struct DoseRecord {
    medication: Arc<Medication>,   // wrong
}

// ✅ Reference by identity only
pub struct DoseRecord {
    medication_id: MedicationId,   // correct — load the aggregate when needed
}
```

---

## 4. Domain Event

> A record that something meaningful happened in the domain. Events are immutable facts
> in the past tense.

### Characteristics

- **Immutable** — an event describes something that already happened.
- **Named in past tense**`DoseTaken`, `MedicationCreated`.
- **Collected, then dispatched** — aggregate methods push events onto an internal queue;
  the application service dispatches them after saving.

### Defining events

```rust
// src/domain/events.rs

#[derive(Debug, Clone)]
pub enum DomainEvent {
    MedicationCreated {
        medication_id: MedicationId,
        name: String,
    },
    DoseTaken {
        dose_record_id: DoseRecordId,
        medication_id: MedicationId,
        taken_at: NaiveDateTime,
    },
    DoseMissed {
        dose_record_id: DoseRecordId,
        medication_id: MedicationId,
        scheduled_at: NaiveDateTime,
    },
}
```

### Collecting events in the aggregate root

```rust
pub struct Medication {
    id: MedicationId,
    name: MedicationName,
    dosage: Dosage,
    scheduled_time: Vec<ScheduledTime>,
    events: Vec<DomainEvent>,   // internal event queue
}

impl Medication {
    pub fn new(id: MedicationId, name: MedicationName, dosage: Dosage, scheduled_time: Vec<ScheduledTime>) -> Self {
        let mut med = Self { id, name, dosage, scheduled_time, events: vec![] };
        med.events.push(DomainEvent::MedicationCreated {
            medication_id: med.id.clone(),
            name: med.name.value().to_owned(),
        });
        med
    }

    /// Drains and returns all pending domain events.
    pub fn drain_events(&mut self) -> Vec<DomainEvent> {
        std::mem::take(&mut self.events)
    }
}
```

### Dispatching from the application service

```rust
// application/services/create_medication.rs

pub async fn execute(&self, cmd: CreateMedicationCommand) -> Result<MedicationId, ApplicationError> {
    let mut medication = Medication::new(/* ... */);
    let id = medication.id().clone();

    self.repository.save(&medication).await?;

    // Dispatch only after a successful save
    for event in medication.drain_events() {
        self.event_bus.publish(event).await?;
    }

    Ok(id)
}
```

---

## 5. Repository

> A collection-like interface for loading and storing aggregates. The domain defines the
> contract; infrastructure provides the implementation.

### The port (domain/application layer)

```rust
// src/application/ports/medication_repository_port.rs

#[async_trait::async_trait]
pub trait MedicationRepositoryPort: Send + Sync {
    async fn save(&self, medication: &Medication) -> Result<(), RepositoryError>;
    async fn find_by_id(&self, id: &MedicationId) -> Result<Option<Medication>, RepositoryError>;
    async fn find_all(&self) -> Result<Vec<Medication>, RepositoryError>;
    async fn delete(&self, id: &MedicationId) -> Result<(), RepositoryError>;
}
```

### Key rules

- One repository per **aggregate root** — never per entity or value object.
- Returns domain types (`Medication`), not persistence models (`MedicationRow`).
- The trait lives in `application/ports/` — it belongs to the domain/application side,
  not infrastructure.

### The adapter (infrastructure layer)

```rust
// src/infrastructure/persistence/json_medication_repository.rs

pub struct JsonMedicationRepository {
    path: PathBuf,
}

#[async_trait::async_trait]
impl MedicationRepositoryPort for JsonMedicationRepository {
    async fn save(&self, medication: &Medication) -> Result<(), RepositoryError> {
        // map Medication → JSON, write to disk
        // infrastructure concern — the domain never knows about files
    }

    async fn find_by_id(&self, id: &MedicationId) -> Result<Option<Medication>, RepositoryError> {
        // read JSON → map to Medication
    }
}
```

### In-memory fake for tests

```rust
// src/application/ports/fakes.rs

pub struct FakeMedicationRepository {
    store: Mutex<HashMap<MedicationId, Medication>>,
}

#[async_trait::async_trait]
impl MedicationRepositoryPort for FakeMedicationRepository {
    async fn save(&self, medication: &Medication) -> Result<(), RepositoryError> {
        self.store.lock().unwrap().insert(medication.id().clone(), medication.clone());
        Ok(())
    }
    // ...
}
```

The fake satisfies the same contract as the real adapter, enabling fully in-memory unit
tests.

---

## 6. Domain Service

> A stateless operation that belongs to the domain but doesn't naturally fit on a single
> entity or value object.

Use a domain service when an operation:

- Involves **multiple aggregates**.
- Has no obvious "home" entity.
- Must remain **free of infrastructure concerns**.

### Example — `DoseScheduler`

```rust
// src/domain/services/dose_scheduler.rs

pub struct DoseScheduler;

impl DoseScheduler {
    /// Generates all DoseRecords due for a Medication on a given day.
    pub fn schedule_for_day(medication: &Medication, date: NaiveDate) -> Vec<DoseRecord> {
        medication
            .scheduled_time()
            .iter()
            .map(|time| {
                let scheduled_at = date.and_hms_opt(time.hour(), time.minute(), 0).unwrap();
                DoseRecord::new(medication.id().clone(), scheduled_at)
            })
            .collect()
    }
}
```

This logic involves both `Medication` and `DoseRecord` without belonging to either.

### Domain service vs application service

| | Domain Service | Application Service |
|---|---|---|
| Layer | `domain/services/` | `application/services/` |
| Dependencies | Domain types only | Ports (repos, clocks, notifiers) |
| State | Stateless | Stateless |
| I/O | None | Via injected ports |
| Purpose | Domain logic | Orchestration |

---

## 7. Factory

> Encapsulates complex creation logic for aggregates or value objects.

When a constructor becomes too complex — especially when involving multiple validation
steps or when creation logic varies by context — extract a factory.

### Simple factory function

```rust
// src/domain/factories/medication_factory.rs

pub struct CreateMedicationInput {
    pub name: String,
    pub amount_mg: u32,
    pub scheduled_time: Vec<(u8, u8)>,  // (hour, minute) pairs
}

pub fn create_medication(input: CreateMedicationInput) -> Result<Medication, DomainError> {
    let name = MedicationName::new(&input.name)?;
    let dosage = Dosage::new(input.amount_mg)?;
    let times = input.scheduled_time
        .into_iter()
        .map(|(h, m)| ScheduledTime::new(h, m))
        .collect::<Result<Vec<_>, _>>()?;

    Ok(Medication::new(MedicationId::generate(), name, dosage, times))
}
```

### Builder pattern for complex aggregates

When aggregates have many optional fields, the Builder pattern prevents telescoping
constructors:

```rust
pub struct MedicationBuilder {
    name: Option<MedicationName>,
    dosage: Option<Dosage>,
    scheduled_time: Vec<ScheduledTime>,
}

impl MedicationBuilder {
    pub fn new() -> Self {
        Self { name: None, dosage: None, scheduled_time: vec![] }
    }

    pub fn name(mut self, name: MedicationName) -> Self {
        self.name = Some(name);
        self
    }

    pub fn dosage(mut self, dosage: Dosage) -> Self {
        self.dosage = Some(dosage);
        self
    }

    pub fn scheduled_time(mut self, time: ScheduledTime) -> Self {
        self.scheduled_time.push(time);
        self
    }

    pub fn build(self) -> Result<Medication, DomainError> {
        Ok(Medication::new(
            MedicationId::generate(),
            self.name.ok_or(DomainError::EmptyMedicationName)?,
            self.dosage.ok_or(DomainError::InvalidDosage)?,
            self.scheduled_time,
        ))
    }
}

// Usage
let medication = MedicationBuilder::new()
    .name(MedicationName::new("Aspirin")?)
    .dosage(Dosage::new(500)?)
    .scheduled_time(ScheduledTime::new(8, 0)?)
    .build()?;
```

---

## 8. Bounded Context

> An explicit boundary within which a particular domain model applies. Different contexts
> may model the same concept differently.

In Rust, bounded contexts map to **crates** (for strict separation) or **top-level
modules** with controlled visibility.

### Module-level context separation

```
src/
  medications/      ← Medication Management context
    domain/
    application/
    infrastructure/
  notifications/    ← Notification context (different model of Medication)
    domain/
    application/
    infrastructure/
```

### Context mapping with an Anti-Corruption Layer (ACL)

When two contexts need to communicate, an ACL translates between their models:

```rust
// notifications/infrastructure/acl/medication_acl.rs

use crate::medications::domain::entities::medication::Medication as MedContextMedication;
use crate::notifications::domain::entities::reminder_target::ReminderTarget;

pub struct MedicationAcl;

impl MedicationAcl {
    /// Translates a Medication from the Medication context into a ReminderTarget
    /// understood by the Notification context.
    pub fn to_reminder_target(med: &MedContextMedication) -> ReminderTarget {
        ReminderTarget {
            label: med.name().value().to_owned(),
            times: med.scheduled_time().to_vec(),
        }
    }
}
```

Neither context's domain model leaks into the other.

---

## 9. Ubiquitous Language

> Use the same language in code as domain experts use when speaking. If a pharmacist says
> "schedule a dose", the code should say `schedule_dose`, not `insert_record` or
> `add_item`.

### Naming examples

| Domain expert says | ❌ Generic code name | ✅ Ubiquitous language |
|---|---|---|
| "Mark the dose as taken" | `update_status(true)` | `mark_taken(at)` |
| "Prescribe a medication" | `add_medication()` | `prescribe()` |
| "The dose was missed" | `status = Missed` | `DoseMissed` (event) |
| "Scheduled administration time" | `time_slot` | `ScheduledTime` |
| "The prescribed amount" | `quantity` | `Dosage` |

### Enforce it in types

```rust
// ❌ Generic
pub fn update(&mut self, status: bool) { /* ... */ }

// ✅ Speaks the domain language
pub fn mark_taken(&mut self, at: NaiveDateTime) -> Result<(), DomainError> { /* ... */ }
// (future: mark_missed)
```

Method names should read like sentences in the domain's vocabulary.

---

## 10. Summary

| DDD Concept | Rust mechanism | Location |
|---|---|---|
| **Value Object** | `struct` with private fields, `PartialEq`, `fn new() -> Result` | `domain/value_objects/` |
| **Entity** | `struct` with `id` field, `&mut self` state transitions | `domain/entities/` |
| **Aggregate Root** | Entity that owns child objects; sole mutation entry point | `domain/entities/` |
| **Domain Event** | `enum` variants, past tense, collected in aggregate | `domain/events.rs` |
| **Repository** | `trait` (port) + concrete `impl` (adapter) | port: `application/ports/`, impl: `infrastructure/persistence/` |
| **Domain Service** | Stateless `struct` with pure functions, no I/O | `domain/services/` |
| **Factory** | Free function or Builder struct for complex creation | `domain/factories/` |
| **Bounded Context** | Crate or top-level module with controlled `pub` visibility | crate root or `src/<context>/` |
| **Ubiquitous Language** | Type names, method names, enum variants match domain vocabulary | everywhere |

### The golden rules

1. **Domain types self-validate** — if a value can be constructed, it is valid.
2. **Aggregates own their boundaries** — external code uses IDs, not references.
3. **Repositories operate on whole aggregates** — never on individual child entities.
4. **Domain and application layers have no I/O** — infrastructure is always behind a trait.
5. **Speak the domain language** — name types and methods after domain concepts, not
   technical operations.