Choose how modules talk¶
Modules never import each other. Pick the mechanism by what the caller needs:
| You need to | Use | Guide |
|---|---|---|
| tell others something happened | an event | Add an event |
| react to something another module did | an @on consumer |
Consume an event |
| ask another module now | a contract | Provide and call a contract |
| show a list that spans modules | a projection, never a request-time join | Build a projection |
| run a process across modules, with undo | a saga owned by one module | Add a saga |
Event or contract?¶
Most choices come down to these two.
| Event | Contract | |
|---|---|---|
| The caller | moves on | waits for the answer |
| Timing | published after commit, handled later | runs during the call |
| Delivery | at least once, so a consumer may see a duplicate; @on claims each event once |
one call; a command is deduplicated by its idempotency key |
| Transaction | atomic with the producer's write, through the outbox | runs in the provider's own unit, not atomic with the caller's |
| If the other side is down | the default streams bus delivers the event later, within stream retention; the volatile redis and memory buses reach only live subscribers |
the call fails with a 503 |
| Direction | the producer does not know who listens | the caller names the provider |
Prefer an event when the other module only needs to know something happened. Use a contract when the caller cannot continue without the answer.
Referring to another module's data¶
Store the other module's id as a plain UUID, never as a foreign key. A module may later move to its own database, and a foreign key could not follow it.