Skip to content

When read model resolution fails

A command that loads a read model can fail for two different kinds of reasons. The request can be wrong: no usable key, or an entity that does not exist. The application can be wrong: a read model no integration owns, or a lookup outside a validator. Arc keeps the two apart. Wrong input answers 400 with a validation result, and a misconfiguration fails loudly instead of posing as a missing entity.

MessageCauseCaller sees
A command key is required for <Type>The command has no usable key400
<Type> was not found for the command keyThe key is valid, but a required read model does not exist400
Expected one read-model resolver for <Type>, found 0No integration owns the typebuild() fails
Expected one read-model resolver for <Type>, found 2Two integrations claim the typebuild() fails
Command read models can only be resolved during command validationreadModelForValidation was called outside a running validatorThe call throws

Both 400 answers carry one validation result with error severity, reason rule, and no members. The command’s own code does not run.

The command resolved no key, so there is nothing to look the read model up by. The key comes from getEventSourceId(), a getKey() method, or a @key() field; see Resolving the event source ID. An empty string counts as no key.

Making the parameter optional does not help: commandReadModel(Type, { optional: true }) still rejects a command without a key, because the lookup cannot be performed at all.

Fix: declare the key on the command.

The key is valid, no instance exists for it, and the parameter is required. Arc treats this as invalid input: the command targets an entity that is not there.

Fix: pick the one that matches your intent.

  • Absence is a business condition. Declare commandReadModel(Type, { optional: true }), or read it with readModelForValidation(Type, { optional: true }) in a validator, and write the rule around null. You get your own message instead of the generic one.
  • The state is required. Keep the parameter required. The 400 is the intended behavior.

A projection runs after the event is appended. A command sent straight after the event that creates the entity can arrive before the read model exists. For a rule that must hold regardless of timing, use a Chronicle constraint.

build() checks every commandReadModel(Type) binding in handle() and provide() before the application serves anything. A type needs exactly one owner:

  • Chronicle owns a type that is a read model in its catalog: projected with @fromEvent or another projection decorator, or targeted by a projection or reducer, and registered through discover() or add(). Discovery can run before or after withChronicle; a class not registered at all is not in the catalog.
  • MongoDB owns a type listed in withMongoDB({ readModels }).
  • An application can add its own ReadModelForCommandResolver with builder.addReadModelForCommandResolver(...).

Two owners fail too, instead of one silently winning. Remove the type from one integration.

readModelForValidation is resolved when the validator runs, not at build(). A missing owner there makes the rule throw, and the caller gets a 400 with reason validatorFailed.

readModelForValidation reads the key and tenant from the validator that is running. Called anywhere else, such as in handle() or a query, it has no command to read from and throws. Use commandReadModel(Type) in handle() and provide().