Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/buf.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@ jobs:
with:
push: false
breaking: ${{ github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'buf:skip:breaking') }}
breaking_against: ${{ github.event.repository.clone_url }}#branch=main,ref=HEAD~1
breaking_against: ${{ github.event.repository.clone_url }}#branch=main
49 changes: 36 additions & 13 deletions docs/explanation/actor-identity.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,44 @@ At application boundaries, the caller may be represented as an authenticated pri

## Choosing Actor or ActorId

Prefer `Actor` for shared event metadata and any field where consumers may need to distinguish users, services, systems, schedulers, integrations, or other actor classes. This is the safer default for reusable event envelopes because the actor taxonomy remains part of the field contract.
Use `ActorId` when the actor can be represented by one canonical identifier. The value may be a prefixed id or resource-name-like string, such as `users/usr_123`, `services/scheduler`, `systems/cron`, or `integrations/github`.

Use `ActorId` only when the surrounding event contract already fixes the actor class and the string identifier alone is the durable domain fact. Do not use `ActorId` just to avoid the `type` field in JSON.
Use `Actor` when consumers need the actor class and local id as separate fields. This is useful when a service wants to keep the actor taxonomy explicit instead of encoding it in one reference value.

For example, common event metadata should usually use `Actor`:
For example, generic event metadata can use `ActorId` when the platform standardizes self-describing actor identifiers:

```protobuf
message EventMetadata {
trogon.actor.v1alpha1.Actor actor = 1;
trogon.actor.v1alpha1.ActorId actor = 1;
trogon.actor.v1alpha1.ActorId on_behalf_of = 2;
}
```

The protobuf JSON shape is object-based:

```json
{
"actor": {
"value": "users/usr_123"
},
"onBehalfOf": {
"value": "services/support-console"
}
}
```

In that contract, `ActorId` already owns the actor-reference concept. A separate `ActorRef` type would describe the same concept with a different name.

The field inside `ActorId` is named `value` because the message itself carries the domain role. Renaming it to `ref` would make JSON and generated APIs churn without adding a separate concept.

Use `Actor` when the split is part of the contract:

```protobuf
message EventMetadata {
trogon.actor.v1alpha1.Actor actor = 1;
}
```

```json
{
"actor": {
Expand All @@ -33,7 +57,7 @@ The protobuf JSON shape is object-based:

`id` is an opaque identifier scoped by `type`. It is not display text, and consumers should compare actors by both fields.

An event-specific field can use `ActorId` when the domain fact is only the identifier and the actor class is already implied:
An event-specific field can also use `ActorId` when the domain fact is the canonical actor identifier:

```protobuf
message AccountCreated {
Expand All @@ -46,12 +70,12 @@ The protobuf JSON shape for `ActorId` is:
```json
{
"createdBy": {
"value": "usr_123"
"value": "users/usr_123"
}
}
```

Changing a field from `ActorId` to `Actor` later changes the protobuf and JSON shape for that field. Choose `Actor` up front when a later type distinction would belong to the same event contract.
Changing a field from `ActorId` to `Actor` later changes the protobuf and JSON shape for that field. Choose `Actor` up front when consumers need separate actor fields rather than one canonical actor identifier.

## Object ID Values

Expand All @@ -61,15 +85,14 @@ Use `Actor` or `ActorId` when the field answers who caused the event:

```protobuf
message AccountCreated {
trogon.actor.v1alpha1.Actor created_by = 1;
trogon.actor.v1alpha1.ActorId created_by = 1;
}
```

```json
{
"createdBy": {
"type": "user",
"id": "usr_123"
"value": "users/usr_123"
}
}
```
Expand All @@ -79,16 +102,16 @@ Do not use `ActorId` for fields that identify the object being created, updated,
```protobuf
message AccountCreated {
string account_id = 1;
trogon.actor.v1alpha1.Actor created_by = 2;
trogon.actor.v1alpha1.ActorId created_by = 2;
}
```

If the event contract already implies the actor type, an object-id string can be carried by `ActorId`:
If the event contract already implies the actor namespace, a shorter id can still be carried by `ActorId`:

```protobuf
message UserProfileUpdated {
trogon.actor.v1alpha1.ActorId updated_by = 1;
}
```

In that case, the value is still an actor identifier in the event, not a general object reference.
In that case, the value is still an actor identifier in the event, not a general object reference. Prefer self-describing values in shared event envelopes so replay, projection, and audit consumers do not need external context to interpret the actor.
15 changes: 8 additions & 7 deletions proto/trogon/actor/v1alpha1/actor.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ import "elixirpb.proto";

option (elixirpb.file).module_prefix = "TrogonProto.Actor.V1Alpha1";

// ActorId identifies a domain actor when the actor type is already implied by
// the surrounding contract.
// ActorId is the stable identifier for a domain actor.
//
// Use Actor when event consumers need both type and id. Use ActorId only when
// the identifier alone is the durable domain contract.
// The value should be self-describing within the actor namespace, such as a
// prefixed id or resource-name-like identifier. Use Actor when consumers need the
// actor class and local id as separate fields.
message ActorId {
// value is a stable opaque actor identifier.
// value is the canonical actor identifier.
//
// This value is not display text and is comparable only within the domain
// boundary defined by the surrounding contract.
// This value is not display text. Consumers should compare the complete
// value and should not rely on parsing unless the actor namespace documents
// that contract.
string value = 1;
}

Expand Down
Loading