`ReplicatedEventSourcedActor` assumes it's the single writer for its `persistenceId` on the local node — that's how `_appendOne` can read `highestSeq` and `append` in one mailbox tick without optimistic-concurrency races. Across nodes (different replicas), parallel writes are the whole point and they're handled correctly via vector clocks.
But within ONE node, two actors with the same `persistenceId` violate the invariant:
```ts
system.actorOf(Props.create(() => new MyReplicated('counter-1')), 'a');
system.actorOf(Props.create(() => new MyReplicated('counter-1')), 'b'); // same pid!
```
Both share the same journal and the same in-memory replicaId. Their `_appendOne` calls race; the second one fails with `JournalConcurrencyError` and silently drops the write. In-memory state on each actor diverges.
Fix: detect the second `actorOf` for the same `persistenceId` and either:
A. Throw at preStart — fail fast. User has to choose a different pid or share a single ref.
B. Warn + log — let it through but flag the misconfiguration.
Recommendation: A. This is a correctness invariant, not a soft preference; failing loudly avoids silent data divergence.
Implementation sketch:
The `ReplicatedEventSourcedActor` base class registers itself with a per-system `Map<persistenceId, ActorRef>` on `preStart` and unregisters on `postStop`. Second registration → throw.
Components:
| File |
Task |
| `src/persistence/ReplicatedEventSourcedActor.ts` |
Add the registry + check; throw `Error('duplicate ReplicatedEventSourcedActor for persistenceId X')`. |
| `tests/unit/persistence/replicated/SingleWriter.test.ts` (new) |
Spawn two with the same pid → second throws. |
Estimate: 1-2 days.
Verification:
- Unit test: `actorOf` of a second instance with the same pid throws synchronously (before the actor even starts processing messages).
- Existing tests stay green — they don't double-spawn.
Out of scope:
- Multi-replica coordination (the cluster-wide pattern IS multi-writer; that's not what this issue addresses).
`ReplicatedEventSourcedActor` assumes it's the single writer for its `persistenceId` on the local node — that's how `_appendOne` can read `highestSeq` and `append` in one mailbox tick without optimistic-concurrency races. Across nodes (different replicas), parallel writes are the whole point and they're handled correctly via vector clocks.
But within ONE node, two actors with the same `persistenceId` violate the invariant:
```ts
system.actorOf(Props.create(() => new MyReplicated('counter-1')), 'a');
system.actorOf(Props.create(() => new MyReplicated('counter-1')), 'b'); // same pid!
```
Both share the same journal and the same in-memory replicaId. Their `_appendOne` calls race; the second one fails with `JournalConcurrencyError` and silently drops the write. In-memory state on each actor diverges.
Fix: detect the second `actorOf` for the same `persistenceId` and either:
A. Throw at preStart — fail fast. User has to choose a different pid or share a single ref.
B. Warn + log — let it through but flag the misconfiguration.
Recommendation: A. This is a correctness invariant, not a soft preference; failing loudly avoids silent data divergence.
Implementation sketch:
The `ReplicatedEventSourcedActor` base class registers itself with a per-system `Map<persistenceId, ActorRef>` on `preStart` and unregisters on `postStop`. Second registration → throw.
Components:
Estimate: 1-2 days.
Verification:
Out of scope: