Event Service Agent Kata

ADR-0001: Topology — Modular Monolith vs Minimal Services

Status: Accepted

Problem

Context

Evaluation Criteria

Options (Topologies with groupings and flows)


1. Inside: API, Orchestration, Timer, Execution — Outside: UI

flowchart LR
	%% Boundary-neutral comms: Broker and HTTP endpoints outside groups
	UI[UI]
	Broker[Message Broker]
	HTTP[HTTP Boundary]
	DB[(Domain DB)]

	subgraph Inside[Modular Monolith]
		API[API]
		ORCH[Orchestration]
		TIMER[Timer]
		EXEC[Execution]
	end

	%% HTTP always crosses boundary via HTTP node
	UI -- request/response --> HTTP
	HTTP -- maps to handlers --> API

	%% All inter-module commands/events bridge via Broker (even if in-process)
	API -- SubmitServiceCall (command) --> Broker
	Broker -- command --> ORCH
	ORCH -- writes --> DB
	ORCH -- ScheduleTimer (command) --> Broker
	Broker -- DueTimeReached (event) --> ORCH
	ORCH -- StartExecution (command) --> Broker
	Broker -- Execution* (events) --> ORCH
	API -- read --> DB
Trade-offs

2. Inside: Orchestration, Timer, Execution — Outside: UI, API

flowchart LR
	UI[UI]
	HTTP[HTTP Boundary]
	Broker[Message Broker]
	DB[(Domain DB)]

	subgraph Outside[Edge]
		API[API]
	end
	subgraph Inside[Core Monolith]
		ORCH[Orchestration]
		TIMER[Timer]
		EXEC[Execution]
	end

	UI -- request/response --> HTTP
	HTTP -- maps to handlers --> API

	API -- SubmitServiceCall (command) --> Broker
	Broker -- command --> ORCH
	ORCH -- writes --> DB
	ORCH -- ScheduleTimer (command) --> Broker
	Broker -- DueTimeReached (event) --> ORCH
	ORCH -- StartExecution (command) --> Broker
	Broker -- Execution* (events) --> ORCH
	API -- read --> DB
Trade-offs

3. Inside: Orchestration, Timer — Outside: UI, API, Execution

flowchart LR
	UI[UI]
	HTTP[HTTP Boundary]
	Broker[Message Broker]
	DB[(Domain DB)]

	subgraph Outside[Edge + Worker]
		API[API]
		EXEC[Execution]
	end
	subgraph Inside[Core Monolith]
		ORCH[Orchestration]
		TIMER[Timer]
	end

	UI -- request/response --> HTTP
	HTTP -- maps to handlers --> API

	API -- SubmitServiceCall (command) --> Broker
	Broker -- command --> ORCH
	ORCH -- writes --> DB
	ORCH -- ScheduleTimer (command) --> Broker
	Broker -- DueTimeReached (event) --> ORCH
	ORCH -- StartExecution (command) --> Broker
	EXEC -- Execution* (events) --> Broker
	Broker -- Execution* (events) --> ORCH
	API -- read --> DB
Trade-offs

4. Inside: none — Outside: UI, API, Orchestration, Timer, Execution (full services)

flowchart LR
	UI[UI]
	HTTP[HTTP Boundary]
	Broker[Message Broker]
	DB[(Domain DB)]

	API[API]
	ORCH[Orchestration]
	TIMER[Timer]
	EXEC[Execution]

	UI -- request/response --> HTTP
	HTTP -- maps to handlers --> API

	API -- SubmitServiceCall (command) --> Broker
	Broker -- command --> ORCH
	ORCH -- writes --> DB
	ORCH -- ScheduleTimer (command) --> Broker
	Broker -- DueTimeReached (event) --> ORCH
	ORCH -- StartExecution (command) --> Broker
	EXEC -- Execution* (events) --> Broker
	Broker -- Execution* (events) --> ORCH
	API -- read --> DB
Trade-offs

Cross-Cutting Considerations

Migration/Extraction Paths (from Option 1)

  1. Extract Execution first: run Execution as a worker consuming StartExecution and emitting Execution*; keep Orchestration unchanged.
  2. Extract API next: edge becomes separate; continues to publish SubmitServiceCall and read from DB.
  3. Add Timer service only if broker delays are insufficient or accuracy/backlog constraints require it.

Open Questions (to close before acceptance)

Decision

Modular Monolith for the MVP, with a real message broker from day one and SQLite as the domain database choose Option 1

Inside: API, Orchestration, Timer, Execution.

Outside: UI.

All module communications (commands/events) go through the broker to preserve semantics and ease future extraction.

Keep Orchestration co-located with the DB to maintain the single-writer constraint and simplify the transactional outbox. Use ports and Effect layers to enforce boundaries and enable evolvability.

Rationale

Evolution Path (summary)

Consequences

Positive

Negative / Risks

Mitigations

Follow-ups

References