$npx -y skills add Aaronontheweb/dotnet-skills --skill akka-best-practicesCritical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.
| 1 | # Akka.NET Best Practices |
| 2 | |
| 3 | ## When to Use This Skill |
| 4 | |
| 5 | Use this skill when: |
| 6 | - Designing actor communication patterns |
| 7 | - Deciding between EventStream and DistributedPubSub |
| 8 | - Implementing error handling in actors |
| 9 | - Understanding supervision strategies |
| 10 | - Choosing between Props patterns and DependencyResolver |
| 11 | - Designing work distribution across nodes |
| 12 | - Creating testable actor systems that can run with or without cluster infrastructure |
| 13 | - Abstracting over Cluster Sharding for local testing scenarios |
| 14 | |
| 15 | ## Reference Files |
| 16 | |
| 17 | - [work-distribution-patterns.md](work-distribution-patterns.md): Database queues, Akka.Streams throttling, outbox pattern |
| 18 | - [cluster-local-abstractions.md](cluster-local-abstractions.md): GenericChildPerEntityParent, IPubSubMediator, execution mode wiring |
| 19 | - [async-cancellation-patterns.md](async-cancellation-patterns.md): Actor-scoped CancellationToken, linked CTS, timeout handling |
| 20 | |
| 21 | --- |
| 22 | |
| 23 | ## 1. EventStream vs DistributedPubSub |
| 24 | |
| 25 | ### Critical: EventStream is LOCAL ONLY |
| 26 | |
| 27 | `Context.System.EventStream` is **local to a single ActorSystem process**. It does NOT work across cluster nodes. |
| 28 | |
| 29 | ```csharp |
| 30 | // BAD: This only works on a single server |
| 31 | // When you add a second server, subscribers on server 2 won't receive events from server 1 |
| 32 | Context.System.EventStream.Subscribe(Self, typeof(PostCreated)); |
| 33 | Context.System.EventStream.Publish(new PostCreated(postId, authorId)); |
| 34 | ``` |
| 35 | |
| 36 | **When EventStream is appropriate:** |
| 37 | - Logging and diagnostics within a single process |
| 38 | - Local event bus for truly single-process applications |
| 39 | - Development/testing scenarios |
| 40 | |
| 41 | ### Use DistributedPubSub for Multi-Node |
| 42 | |
| 43 | For events that must reach actors across multiple cluster nodes, use `Akka.Cluster.Tools.PublishSubscribe`: |
| 44 | |
| 45 | ```csharp |
| 46 | using Akka.Cluster.Tools.PublishSubscribe; |
| 47 | |
| 48 | public class TimelineUpdatePublisher : ReceiveActor |
| 49 | { |
| 50 | private readonly IActorRef _mediator; |
| 51 | |
| 52 | public TimelineUpdatePublisher() |
| 53 | { |
| 54 | // Get the DistributedPubSub mediator |
| 55 | _mediator = DistributedPubSub.Get(Context.System).Mediator; |
| 56 | |
| 57 | Receive<PublishTimelineUpdate>(msg => |
| 58 | { |
| 59 | // Publish to a topic - reaches all subscribers across all nodes |
| 60 | _mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update)); |
| 61 | }); |
| 62 | } |
| 63 | } |
| 64 | ``` |
| 65 | |
| 66 | ### Akka.Hosting Configuration for DistributedPubSub |
| 67 | |
| 68 | ```csharp |
| 69 | builder.WithDistributedPubSub(role: null); // Available on all roles, or specify a role |
| 70 | ``` |
| 71 | |
| 72 | ### Topic Design Patterns |
| 73 | |
| 74 | | Pattern | Topic Format | Use Case | |
| 75 | |---------|--------------|----------| |
| 76 | | Per-user | `timeline:{userId}` | Timeline updates, notifications | |
| 77 | | Per-entity | `post:{postId}` | Post engagement updates | |
| 78 | | Broadcast | `system:announcements` | System-wide notifications | |
| 79 | | Role-based | `workers:rss-poller` | Work distribution | |
| 80 | |
| 81 | --- |
| 82 | |
| 83 | ## 2. Supervision Strategies |
| 84 | |
| 85 | ### Key Clarification: Supervision is for CHILDREN |
| 86 | |
| 87 | A supervision strategy defined on an actor dictates **how that actor supervises its children**, NOT how the actor itself is supervised. |
| 88 | |
| 89 | ```csharp |
| 90 | public class ParentActor : ReceiveActor |
| 91 | { |
| 92 | // This strategy applies to children of ParentActor, NOT to ParentActor itself |
| 93 | protected override SupervisorStrategy SupervisorStrategy() |
| 94 | { |
| 95 | return new OneForOneStrategy( |
| 96 | maxNrOfRetries: 10, |
| 97 | withinTimeRange: TimeSpan.FromSeconds(30), |
| 98 | decider: ex => ex switch |
| 99 | { |
| 100 | ArithmeticException => Directive.Resume, |
| 101 | NullReferenceException => Directive.Restart, |
| 102 | ArgumentException => Directive.Stop, |
| 103 | _ => Directive.Escalate |
| 104 | }); |
| 105 | } |
| 106 | } |
| 107 | ``` |
| 108 | |
| 109 | ### Default Supervision Strategy |
| 110 | |
| 111 | The default `OneForOneStrategy` already includes rate limiting: |
| 112 | - **10 restarts within 1 second** = actor is permanently stopped |
| 113 | - This prevents infinite restart loops |
| 114 | |
| 115 | **You rarely need a custom strategy** unless you have specific requirements. |
| 116 | |
| 117 | ### When to Define Custom Supervision |
| 118 | |
| 119 | **Good reasons:** |
| 120 | - Actor throws exceptions indicating irrecoverable state corruption -> Restart |
| 121 | - Actor throws exceptions that should NOT cause restart (expected failures) -> Resume |
| 122 | - Child failures should affect siblings -> Use `AllForOneStrategy` |
| 123 | - Need different retry limits than the default |
| 124 | |
| 125 | **Bad reasons:** |
| 126 | - "Just to be safe" - the default is already safe |
| 127 | - Don't understand what the actor does - understand it first |
| 128 | |
| 129 | --- |
| 130 | |
| 131 | ## 3. Error Handling: Supervision vs Try-Catch |
| 132 | |
| 133 | ### When to Use Try-Catch (Most Cases) |
| 134 | |
| 135 | **Use try-catch when:** |
| 136 | - The failure is **expected** (network timeout, invalid input, external service down) |
| 137 | - You know **exactly why** the exception occurred |
| 138 | - You can handle it **gracefully** (retry, return error response, log and contin |