Grain
Origin and Philosophy
The Grain abstraction in GoAkt is inspired by the Virtual Actor Model from Microsoft Orleans. However, GoAkt adapts this concept to Go’s concurrency model with minimalistic and composable design principles.
Grains in GoAkt are location-transparent, message-driven entities with unique identities. Unlike traditional actors, Grains in GoAkt have no internal lifecycle logic—they're managed entirely by the actor system.
What is Grain in GoAkt?
A Grain is a specialised actor that:
Has a globally unique identity
Is location-transparent across local and clustered deployments
Can be stateful or stateless
Automatically activated when needed
Automatically removed from memory when inactive after a period of time.
Is lightweight and consumes no resources when inactive
Lives as long as the actor system unless explicitly deactivated
Can communicate with other Grains or Actors
Characteristics
Unique Identity
Every Grain has a consistent, system-wide identity that is used to communicate with the Grain
Stateless or Stateful
Stateless by default; statefulness is implemented via Extensions
No Lifecycle API
No Spawn or Shutdown like actors; system manages creation and shutdown
Automatic Activation
Grains are automatically activated when requesting their identity.
Activation/Deactivation
OnActivate and OnDeactivate hooks for setup and teardown
Inactive Resource Consumption
Inactive Grains consume no CPU or memory
Automatic Deactivation
Grains are automatically deactivated and removed from memory when idle after a certain period of time that can be configured. The default is two minutes
Explicit Deactivation
Send PoisonPill to explicitly deactivate a Grain
Cluster-Aware Relocation
Grains are automatically redeployed on other nodes (unless disabled)
Message-Passing Model
Communicate using Tell (fire-and-forget) or Ask (request/response)
Actor Interoperability
Grains and Actors can message each other seamlessly
Get Started
Grain interface
To define a Grain one needs to implement the Grain interface:
type Grain interface {
// OnActivate is called when the grain is loaded into memory.
// Use this to initialize state or resources.
//
// Arguments:
// - ctx: context for cancellation and deadlines.
// - props: grain properties and system-level references.
// Returns:
// - error: non-nil to indicate activation failure (grain will not be activated).
OnActivate(ctx context.Context, props *GrainProps) error
// OnReceive is called when the grain receives a message.
//
// Arguments:
// - ctx: GrainContext containing the message, sender, grain identity, and system references.
// Behavior:
// - Processes the message and updates grain state as needed.
// - Always respect cancellation and deadlines via the context in GrainContext.
// - Do not retain references to the GrainContext or its fields beyond the method scope.
OnReceive(ctx *GrainContext)
// OnDeactivate is called before the grain is removed from memory.
// Use this to persist state and release resources.
//
// Arguments:
// - ctx: context for cancellation and deadlines.
// - props: grain properties and system-level references.
// Returns:
// - error: non-nil to indicate deactivation failure (system may log or handle the failure).
OnDeactivate(ctx context.Context, props *GrainProps) error
}Grain Identity
Once the Grain interface is implemented, you will need to acquire or create an identity for each instance of the given Grain using the ActorSystem method GrainIdentity .
Activation Strategies
Grain activation is location-agnostic by default, but you can steer placement using these strategies:
RoundRobinActivation – Cycles through nodes in sequence to spread activations evenly over time. ⚠️ Applied only if the Grain doesn’t already exist when cluster mode is on; if it does, activation occurs on the node where it already exists.
RandomActivation – Picks a node at random. Fast and stateless, but can produce uneven load. ⚠️ Applied only if the Grain doesn’t already exist when cluster mode is on; otherwise it activates where the Grain already resides.
LocalActivation – Forces activation on the local node. Useful when locality (e.g., access to local resources) matters. ⚠️ Applied only if the Grain doesn’t already exist when cluster mode is on; otherwise it activates on the node that already has it.
LeastLoadActivation – Chooses the node with the lowest current load to optimise utilisation and responsiveness. ⚠️ May add overhead to gather cluster load metrics. ⚠️ Applied only if the Grain doesn’t already exist when cluster mode is on; otherwise it activates where the Grain already exists.
These activation strategies are options that can be passed to
GrainIdentitymethod usingWithActivationStrategy.
Role-based Activation
By default, activation strategies decide how grains are placed (e.g., round-robin, random, least-load). You can further control where grains are activated by combining those strategies with role-based placement. Roles let you target specific subsets of nodes—such as "projection", "payments", or "api"—so grains only start on machines that advertise the required capability or colocation.
How it works
Nodes advertise roles via cluster configuration (e.g.,
ClusterConfig.WithRoles("projection", "api")).You request a role for activation (e.g.,
WithActivationRole("payments")).Placement is filtered by role first, then your chosen activation strategy is applied among the matching nodes only:
Round-robin cycles through the eligible nodes.
Random picks any eligible node at random.
Least-load picks the least busy of the eligible nodes.
Local forces activation on the local node (only meaningful if the local node has the role).
If clustering is disabled, role constraints are ignored and the grain activates locally.
Failure behaviour
If no node advertises the required role, activation fails with an error. This prevents accidental placement on nodes that lack necessary services or colocation guarantees.
When to use roles
Colocation needs: Keep grains near role-specific services (e.g., a local cache or projector).
Blast radius control: Isolate certain workloads (e.g.,
"payments") to hardened nodes.Resource specialisation: Reserve GPU/IO-optimised nodes for specific grain types.
Tips
Omit
WithActivationRoleto allow placement on any node.If you want universal eligibility, ensure all nodes advertise the role.
Remember that strategies only apply if the grain doesn’t already exist elsewhere in cluster mode; otherwise activation occurs where it already lives.
Example:
grain := &Grain{}
accountID := uuid.NewString()
identity, err := actorSystem.GrainIdentity(ctx, accountID, func(ctx context.Context) (goakt.Grain, error) {
return grain, nil
},
WithActivationStrategy(RoundRobinActivation),
WithActivationRole("backend"))Messaging
Once the grain identity is obtained then you can start messaging the Grain using the following ActorSystem methods:
AskGrain: Send a synchronous request message to the Grain and expects a response or an error. The request is time-bound.TellGrain: sends an asynchronous message to a Grain (virtual actor) identified by the given identity.
Message Handling
Every request sent to a Grain is time-bound.
In the OnReceive message handler of a Grain one can given the GrainContext perform the following actions:
Response(resp proto.Message): This method helps send a response to theAskGrainrequest to the callerErr(err error): To return an error from the handler. This is the recommended way instead of panicking inside the message handler. Even if the message handler does panic, GoAkt will recover smoothly without crashing the application.NoErr(): This method is used to state that the message handler return without any error. Use this method when dealing with theTellGrain.Unhandled(): Unhandled marks the currently received message as unhandled by the Grain.AskActor(name string, message proto.Message, timeout time.Duration) (proto.Message, error): AskActor sends a message to another actor by name and waits for a responseTellActor(actorName string, message proto.Message) error: TellActor sends a message to another actor by name without waiting for a responseAskGrain(to *GrainIdentity, message proto.Message, timeout time.Duration) (proto.Message, error): AskGrain sends a message to another Grain and waits for a response.TellGrain(to *GrainIdentity, message proto.Message) error: TellGrain sends a message to another Grain without waiting for a response.GrainIdentity(name string, factory GrainFactory, opts ...GrainOption) (*GrainIdentity, error): GrainIdentity creates or retrieves a unique identity for a Grain instance.Self() *GrainIdentity: Self returns the unique identifier of the Grain instance.Message() proto.Message: Message returns the message currently being processed by the Grain.Context() context.Context: Context returns the underlying context associated with the GrainContext.ActorSystem() ActorSystem: ActorSystem returns the ActorSystem that manages the Grain.
Statefulness
To make a Grain stateful:
Use the Extension capability to attach persistence storage.
Extensions can implement snapshotting, durable storage, etc.
This keeps the core Grain logic clean and testable.
Example
Kindly check the following links:
Last updated