Architecture Rules
The rules a Bsft application is expected to hold to. They are conventions, not compiler-enforced constraints, but the framework is built so that following them is the path of least resistance.
Rule 1 — one direction of flow
Event → Saga Step → Command → Handler → Event
A saga step reacts to an event and dispatches commands. Business logic lives in the handler behind the command, never in the step and never in the event. A saga is an orchestrator: it decides what happens next, not how it happens.
Consequences:
- A step must not call a repository, write to the database or talk to an external system directly.
- A handler must not know that a saga exists. It answers with an event; whether a saga listens is none of its business.
- A durable timeout publishes an event and does nothing else. The timer is a scheduling mechanism, not a place for logic — see saga-timeouts.md.
Rule 2 — state belongs to its owner
Saga state describes the progress of the orchestration, not the business entity. Order totals, stock levels and customer data live in the module that owns them; the saga only keeps what it needs to route the next step.
Rule 3 — a module owns its tables
A module sees its own tables through its own DbContext interface. The application's DbContext implements those interfaces; a module never reaches into another module's tables, it asks through mediator or events.
Rule 4 — time comes from TimeProvider
No DateTime.UtcNow anywhere in framework or application code. Anything time-dependent must be
testable by advancing a clock rather than by waiting.
Rule 5 — at-least-once means idempotent
Anything delivered by the outbox dispatcher or the saga timeout dispatcher can arrive twice. The handling of such an event must be idempotent. The framework guarantees that a duplicate does not corrupt the saga; it cannot guarantee that a duplicate e-mail is not sent.
Rule 6 — a CRUD resource looks the same everywhere
A resource that is edited directly is exposed as GET/POST /api/{resource} and
GET/PUT/DELETE /api/{resource}/{id}, and every failure is thrown rather than returned, so
BsftExceptionMiddleware maps it to the documented status code — 400 validation, 403 forbidden,
404 not found, 409 conflict, 422 business rule. The routes, the bodies, the status codes and the
placement of validation are one convention rather than a per-module choice, because a generic
client can only be written once. See crud-conventions.md; the reference
implementation is the products resource in samples/Bsft.Sample.Inventory.