Cohesive Systems logoCOHESIVE SYSTEMS

Search Cohesive Systems

Ready

Search Cohesive Systems

Find product pages, building blocks, technical articles, and graph definitions.

Building Blocks

Cohesive.Entities

Define business entities, invariants, and the transitions that change them using familiar, typed C#.

Core Idea

An entity defines authoritative business state: the fields it contains and the invariants every valid state must satisfy. Its Transitions define the business decisions that can change that state.

Cohesive.Transitions lets you describe both in typed C#. A Transition receives the entity's current state and a typed input, checks the relevant rules, and returns a decision. That decision can include state changes, a typed outcome, and effects such as domain events or requests for other work.

Here is a Load with one useful operation. Assigning a carrier is allowed only while the Load is still a draft. A successful decision updates the state and emits a LoadAssigned domain event.

public enum LoadStatus { Draft, Assigned }
public enum AssignCarrierOutcome { Assigned, NotDraft, InvalidCarrier }
 
public sealed record AssignCarrierInput(string CarrierId);
public sealed record LoadAssigned(string CarrierId);
 
public sealed class Load : Entity<Load>
{
    public Load()
    {
        Status = Field(nameof(Status), LoadStatus.Draft);
        CarrierId = Field<string?>(nameof(CarrierId),
            initialValue: null,
            configure: field => field.Optional()
        );
 
        Invariant("AssignedLoadsHaveCarrier",
            load => load.Status != LoadStatus.Assigned || load.CarrierId != null
        );
 
        AssignCarrier = Transition<AssignCarrierInput, AssignCarrierOutcome>(
            LoadSemantics.AssignCarrier,
            transition => transition
                .Requires(
                    (load, _) => load.Status == LoadStatus.Draft,
                    (_, _) => AssignCarrierOutcome.NotDraft
                )
                .Requires(
                    (_, input) => input.CarrierId != "",
                    (_, _) => AssignCarrierOutcome.InvalidCarrier
                )
                .Set(
                    load => load.CarrierId,
                    (_, input) => input.CarrierId
                )
                .Set(
                    load => load.Status,
                    LoadStatus.Assigned
                )
                .Emit(
                    LoadInteractions.LoadAssigned,
                    (_, input) => new LoadAssigned(input.CarrierId)
                )
                .Return(
                    TransitionOutcomeDisposition.Applied,
                    AssignCarrierOutcome.Assigned
                ));
    }
 
    public Field<LoadStatus> Status { get; }
    public Field<string?> CarrierId { get; }
    public Transition<Load, AssignCarrierInput, AssignCarrierOutcome> AssignCarrier { get; }
}

LoadSemantics.AssignCarrier supplies stable metadata for the Transition, while LoadInteractions.LoadAssigned identifies the event contract. Those declarations are setup rather than business logic, so the example leaves them out. The Getting Started guide builds the complete version.

Getting Started

The guided walkthrough installs the package and builds the complete AssignCarrier example. It starts with the entity and its business rule, then shows how to evaluate one decision before connecting storage or messaging.

Open the Getting Started guide →

A Small Mental Model

The model follows the way C# developers already tend to discuss domain behavior:

Entity

Question it answers
What business state do we own?
Load example
Status and CarrierId belong to a Load.

Invariant

Question it answers
What must be true of every valid state?
Load example
An assigned Load must have a carrier.

Transition

Question it answers
Which decisions may change that state?
Load example
AssignCarrier can move a draft Load into the assigned state.

Input

Question it answers
What does the caller provide?
Load example
The carrier identity to assign.

Decision

Question it answers
What did the rule decide?
Load example
Assigned, NotDraft, or InvalidCarrier, together with any changes and effects.

The expressions receive load and input directly. Reading load.Status gives the Transition the current business context it needs. No repository, service provider, or ambient request object is hidden inside the rule.

Why Make the Upfront Investment?

Ordinary entity methods are easy to start with. Over time, their rules often spread into handlers, persistence hooks, message publishers, API endpoints, and workflow code. Cohesive asks you to describe each change more explicitly so more of the surrounding system can work from the same definition.

A structured domain model

What it gives you
Cohesive guides each entity into explicit fields, invariants, named Transitions, typed inputs and outcomes, and effects instead of leaving those roles implicit across a POCO and its surrounding services.

Pure unit tests

What it gives you
Supply state and input, evaluate a deterministic decision, and assert its outcome, field changes, and effects without arranging a database, service container, or message broker.

Structured diagnostics

What it gives you
Admission rejections, domain rejections, invariant violations, concurrency conflicts, and invalid definitions remain distinct and attributable instead of collapsing into a boolean, exception, or generic validation message.

Explicit effects

What it gives you
The Transition declares events and requested work without performing hidden I/O. Those effects can integrate with event-sourced storage, a transactional outbox, or a simple message queue, with formal capability diagnostics when the chosen path cannot preserve the required guarantees.

Explicit field assignments

What it gives you
Cohesive knows which fields a Transition may read and change. Adapters can acquire only required state, while evaluated patches can drive audit records and change events.

Automatic concurrency control

What it gives you
Cohesive records which state an invocation used to reach its decision. Storage integrations can validate that state at commit time, detect meaningful stale decisions, and return structured conflict evidence without requiring every handler to implement its own version checks.

One definition, many consumers

What it gives you
Processes can invoke entity Transitions as steps in multi-entity workflows, while APIs, presentation, diagnostics, documentation, and upcoming simulation and automated-testing tools use the same operation and outcome model.

Infrastructure agnostic

What it gives you
The business decision is independent of a particular repository, transaction library, event store, transport, or hosting model, so those choices can evolve around the entity rather than through it.

Transitions Return Decisions

A Transition decides; it does not save an entity or publish a message while the rule is running. Given the same state and input, it reaches the same result.

For AssignCarrier, a decision can say that the Load was assigned and include the two proposed field changes plus the event to emit. It can instead return NotDraft or InvalidCarrier with no accepted state change. The caller receives an explicit result in either case.

This separation keeps business meaning easy to test. Infrastructure can later apply an accepted decision with the transaction, concurrency, and delivery guarantees required by the application.

How a Transition Runs

In an application, an entity Transition runs inside an execution environment. An API operation, command handler, or Cohesive Process supplies the typed input. The environment obtains the current entity state, evaluates the Transition, and receives its decision.

If the decision is accepted, the environment asks the configured storage integration to validate concurrency and commit the field changes. Its effect integration then records and delivers any emitted events or requests through the selected event store, outbox, or message queue. When an integration cannot provide the guarantees the decision requires, execution stops with capability diagnostics.

The entity remains pure because repositories, transactions, clocks, message publishers, and retry policies belong to this surrounding environment. The Internals guide describes the execution and commit boundaries in detail.

Invariants Protect Every Valid State

An entity invariant describes a rule that must hold regardless of which Transition produced the state. In the example, AssignedLoadsHaveACarrier belongs to the Load because it defines what a valid Load is. Entity validation uses that rule whenever it checks or constructs Load state.

A Transition can also add a candidate-state invariant to its own decision definition. Cohesive evaluates that local check before returning an accepted change. This is useful when the Transition must carry the complete protection needed at its execution boundary.

Admission rules answer a different question. requires-draft explains when AssignCarrier may begin, so a non-draft Load returns the typed NotDraft outcome instead of becoming an invariant failure.

Domain Events and Requested Work

Transitions can emit effects as part of an accepted decision. The referenced interaction contract says which kind of effect it is:

  • A domain event records a fact established by the decision, such as LoadAssigned.
  • A request says that another capability owes some work, such as SchedulePickup. Its contract defines the expected response or terminal outcome, which lets a Process coordinate the obligation explicitly.

Emitting an effect does not call a handler from inside the Transition. The decision identifies what should happen and carries the typed payload; the execution environment makes the state change and its outgoing interactions durable under the guarantees the application chooses.

Where the Model Fits

Make assignment and lifecycle rules explicit

A Load can expose transitions such as AssignCarrier, SchedulePickup, Depart, and Deliver, each with the state and input it needs.

  • Reject assignment after dispatch
  • Emit LoadAssigned when the decision succeeds
  • Request pickup scheduling as an explicit obligation
  • Protect lifecycle-wide Load invariants

Works with the Other Blocks

  • Cohesive.Processes invokes Transitions, waits for requested work, and carries durable outcomes forward through a longer-running flow.
  • Cohesive.Relations describes the facts and derived views used before or around a business decision.
  • Cohesive.Storage reads current state and commits accepted changes and outgoing interactions with the required guarantees.
  • Cohesive.Api and Cohesive.Presentation can project the same typed inputs and outcomes into operations and user experiences.

Going Deeper

The C# surface is only the entry point. Cohesive also needs to preserve a Transition across processes and versions, determine exactly which state it reads, validate references, detect conflicts, and hand accepted effects to storage and delivery infrastructure.

Those concerns introduce the canonical Transition document, expression lowering, compilation, sparse observations, semantic identity, execution evidence, and the boundary between a decision and its externalities. They matter once you are evaluating architecture or building adapters, but they are not prerequisites for understanding the domain model.

Read Transition Internals →