Skip to content

Event oriented architecture

Study Setup is modeled around domain events. Rather than describing the system only as screens and database tables, the product behavior is captured as a set of formal Command, Event, and Policy objects — one coherent vocabulary of intents, facts, and reactions that spans every module in this PRD.

Key motivations

  • Single source of truth. Commands, events, and policies form one precise, shared vocabulary used across UI, services, and downstream integrations — reducing ambiguity between product, engineering, and validation.
  • Auditability by construction. Because every meaningful change is an immutable event, the Audit Trail and history features fall out of the model rather than being bolted on.
  • Loose coupling. Modules communicate through events and policies instead of direct calls, so a single action (e.g. approving a changeset) can fan out to verification, provisioning, and notifications without entangling those modules.
  • Explicit features. All key business process concerns correspond to formal classes that can be identified, validated and tested with clear boundaries. This allows a clearer framework for configuring notifications, access control and triggering conditions.
  • Non-eventsourced. We avoid actual eventsourcing in order to avoid complicating the Django-based code. All objects are still "normal" Diango models, in an entity-relation design, with a CRUD based lifecycle (taht is mediated by the Command/Event protocol).

Commands, Events and Policies

ObjectQuestion it answersTenseExample
CommandWhat does an actor want to happen?ImperativeCreateStudy, SetStudyScopedAccess
EventWhat did happen, as an immutable fact?Past tenseStudyCreated, StudyAccessGranted
PolicyWhat should happen in reaction to a fact?ReactiveStudyAvailableForMetadataReview

A command expresses an intent to change state and may be rejected if its validation rules are not met. Once accepted, it produces one or more events.

Events are the durable record of what actually occurred — they are never mutated or deleted – and they drive the actual changes in the Django Models.

Policies observe events and decide which follow-on commands, if any, should be issued, allowing one action to cascade into downstream work without modules being tightly coupled to each other. The policy notification mechanism uses Django Signals under the hood.

Commands — intents to change state

A Command is a request to do something, named in the imperative (CreateStudy, ManagePermissions, GrantExternalReviewerAccess). It carries the parameters needed to carry out the intent and a set of validation criteria — the business rules that must hold for the command to be accepted (e.g. "Actor is authorized PM/SDL", "Study ID must be unique"). A command that fails its validation produces no events.

The API provides a common mechanism for both logging and reporting command validation errors to the end users in a standardized manner.

Events — facts that resulted

An Event is an immutable statement that something happened, named in the past tense (StudyCreated, ChangesetApproved, ExternalReviewerAccessGranted). It records the relevant attributes at the moment of occurrence. Events are the backbone of the model: they feed the Audit Trail, drive Notifications, and are what downstream modules react to.

Policies — reactions to events

A Policy listens for a single event type and decides what should happen next, typically by issuing further commands. Policies are how the system reacts automatically — for example, marking a study as ready for metadata review once it is created, or requiring an artifact refresh before a provisioning release.

Querying

Querying is purposefully modeled using built-in DjangoRestFramework Model-based views. In this sense the Django ORM is the application's "Read Model".

Modules

The objects are organized into modules, which are very close in principle to bounded contexts in Domain Driven Design. We do not follow strict adherence to all DDD patterns, and so refrain from adopting the language where expectations may not fully match.

Events may cross context boundaries — a fact produced in one domain can trigger a policy in another. Cross-domain references are tracked in the model with an owning-domain tag (e.g. ChangesetCreated), keeping the contexts decoupled while still letting work flow between them.