Repository: Tochemey/goakt
Pull request #398 (2024-07-25 21:11:30)

refactor: add a supervisor and router
- Add System supervisor (preparation for escalation directive)
- Add router to distribute messages of the same type over a set of actors
- Add `SpawnRouter` method to `ActorSystem` to create a router
- Add `Logger` method to `ActorSystem` to return the system logger
- Enhance PID `Shutdown` method to properly clean resources.
- Add validation to actor name


Patch:
diff --git a/README.md b/README.md
index 184263b..4616c02 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ The project adheres to [Semantic Versioning](https://semver.org) and [Convention
     - [Actor System](#actor-system)
     - [Behaviors](#behaviors)
     - [Mailbox](#mailbox)
-    - [Routers](#routers)
+    - [Router](#router)
     - [Events Stream](#events-stream)
       - [Supported events](#supported-events)
     - [Messaging](#messaging)
@@ -178,20 +178,21 @@ To change the behavior, call the following methods on the [ReceiveContext interf
 
 Once can implement a custom mailbox. See [Mailbox](./actors/mailbox.go). The default mailbox makes use of buffered channels.
 
-### Routers
+### Router
 
 Routers help send the same type of message to a set of actors to be processed in parallel depending upon
-the type of the router used. Routers should be used with caution because they can hinder performance
-Go-Akt comes shipped with the following routers:
-* `Broadcast router`: This router broadcasts the given message to all its available routees in parallel. When the router receives a message to broadcast, every routee is checked whether alive or not.
-  When a routee is not alive the router removes it from its set of routees. When the last routee stops the router itself stops
-* `Random router`: This type of router randomly pick a routee in its set of routees and send the message to it.
-  Only routees that are alive are considered when the router receives a message. 
-* `Round-Robin router`: This type of router send messages to its routee in a round-robin way. For n messages sent through the router, each actor is forwarded one message.
-  Only routees that are alive are considered when the router receives a message.
-
-The routers are just actors that can be spawned like any other actors. Once the given router has started it will spawn its routees.
-Once a router dies all its routees die with it.
+the type of the router used. Routers should be used with caution because they can hinder performance.
+When the router receives a message to broadcast, every routee is checked whether alive or not.
+When a routee is not alive the router removes it from its set of routees. 
+When the last routee stops the router itself stops.
+
+Go-Akt comes shipped with the following routing strategies:
+* `Fan-Out`: This strategy broadcasts the given message to all its available routees in parallel.
+* `Random`: This strategy randomly picks a routee in its set of routees and send the message to it.
+* `Round-Robin`: This strategy sends messages to its routee in a round-robin way. For n messages sent through the router, each actor is forwarded one message.
+
+A router a just like any other actor that can be spawned. To spawn router just call the [ActorSystem](./actors/actor_system.go) `SpawnRouter` method.
+Router as well as their routees are not passivated.
 
 ### Events Stream
 
diff --git a/actors/actor_system.go b/actors/actor_system.go
index 51c3b80..787a825 100644
--- a/actors/actor_system.go
+++ b/actors/actor_system.go
@@ -31,6 +31,7 @@ import (
 	"net/http"
 	"regexp"
 	"strconv"
+	"strings"
 	"sync"
 	"time"
 
@@ -81,6 +82,10 @@ type ActorSystem interface {
 	// SpawnNamedFromFunc creates an actor with the given receive function and provided name. One can set the PreStart and PostStop lifecycle hooks
 	// in the given optional options
 	SpawnNamedFromFunc(ctx context.Context, name string, receiveFunc ReceiveFunc, opts ...FuncOption) (PID, error)
+	// SpawnRouter creates a new router. One can additionally set the router options.
+	// A router is a special type of actor that helps distribute messages of the same type over a set of actors, so that messages can be processed in parallel.
+	// A single actor will only process one message at a time.
+	SpawnRouter(ctx context.Context, poolSize int, routeesKind Actor, opts ...RouterOption) (PID, error)
 	// Kill stops a given actor in the system
 	Kill(ctx context.Context, name string) error
 	// ReSpawn recreates a given actor in the system
@@ -127,6 +132,8 @@ type ActorSystem interface {
 	Register(ctx context.Context, actor Actor) error
 	// Deregister removes a registered actor from the registry
 	Deregister(ctx context.Context, actor Actor) error
+	// Logger returns the logger sets when creating the actor system
+	Logger() log.Logger
 	// handleRemoteAsk handles a synchronous message to another actor and expect a response.
 	// This block until a response is received or timed out.
 	handleRemoteAsk(ctx context.Context, to PID, message proto.Message, timeout time.Duration) (response proto.Message, err error)
@@ -134,6 +141,8 @@ type ActorSystem interface {
 	handleRemoteTell(ctx context.Context, to PID, message proto.Message) error
 	// setActor sets actor in the actor system actors registry
 	setActor(actor PID)
+	// supervisor return the system supervisor
+	getSupervisor() PID
 }
 
 // ActorSystem represent a collection of actors on a given node
@@ -168,7 +177,7 @@ type actorSystem struct {
 	// The default value is 1s
 	actorInitTimeout time.Duration
 	// Specifies the supervisor strategy
-	supervisorDirective supervisorDirective
+	supervisorDirective SupervisorDirective
 	// Specifies the telemetry config
 	telemetry *telemetry.Telemetry
 	// Specifies whether remoting is enabled.
@@ -222,6 +231,8 @@ type actorSystem struct {
 	peersCache             map[string][]byte
 	clusterConfig          *ClusterConfig
 	redistributionChan     chan *cluster.Event
+
+	supervisor PID
 }
 
 // enforce compilation error when all methods of the ActorSystem interface are not implemented
@@ -299,6 +310,14 @@ func NewActorSystem(name string, opts ...Option) (ActorSystem, error) {
 	return system, nil
 }
 
+// Logger returns the logger sets when creating the actor system
+func (x *actorSystem) Logger() log.Logger {
+	x.locker.Lock()
+	logger := x.logger
+	x.locker.Unlock()
+	return logger
+}
+
 // Deregister removes a registered actor from the registry
 func (x *actorSystem) Deregister(ctx context.Context, actor Actor) error {
 	_, span := x.tracer.Start(ctx, "Deregister")
@@ -400,7 +419,7 @@ func (x *actorSystem) InCluster() bool {
 
 // NumActors returns the total number of active actors in the system
 func (x *actorSystem) NumActors() uint64 {
-	return uint64(x.actors.len())
+	return uint64(len(x.Actors()))
 }
 
 // Spawn creates or returns the instance of a given actor in the system
@@ -435,6 +454,7 @@ func (x *actorSystem) Spawn(ctx context.Context, name string, actor Actor) (PID,
 		return nil, err
 	}
 
+	x.supervisor.Watch(pid)
 	x.setActor(pid)
 	return pid, nil
 }
@@ -459,6 +479,7 @@ func (x *actorSystem) SpawnNamedFromFunc(ctx context.Context, name string, recei
 		return nil, err
 	}
 
+	x.supervisor.Watch(pid)
 	x.setActor(pid)
 	return pid, nil
 }
@@ -468,6 +489,15 @@ func (x *actorSystem) SpawnFromFunc(ctx context.Context, receiveFunc ReceiveFunc
 	return x.SpawnNamedFromFunc(ctx, uuid.NewString(), receiveFunc, opts...)
 }
 
+// SpawnRouter creates a new router. One can additionally set the router options.
+// A router is a special type of actor that helps distribute messages of the same type over a set of actors, so that messages can be processed in parallel.
+// A single actor will only process one message at a time.
+func (x *actorSystem) SpawnRouter(ctx context.Context, poolSize int, routeesKind Actor, opts ...RouterOption) (PID, error) {
+	router := newRouter(poolSize, routeesKind, x.logger, opts...)
+	routerName := x.getSystemActorName(routerType)
+	return x.Spawn(ctx, routerName, router)
+}
+
 // Kill stops a given actor in the system
 func (x *actorSystem) Kill(ctx context.Context, name string) error {
 	spanCtx, span := x.tracer.Start(ctx, "Kill")
@@ -522,6 +552,7 @@ func (x *actorSystem) ReSpawn(ctx context.Context, name string) (PID, error) {
 		}
 
 		x.actors.set(pid)
+		x.supervisor.Watch(pid)
 		return pid, nil
 	}
 
@@ -541,8 +572,15 @@ func (x *actorSystem) Name() string {
 // Actors returns the list of Actors that are alive in the actor system
 func (x *actorSystem) Actors() []PID {
 	x.locker.Lock()
+	pids := x.actors.pids()
 	defer x.locker.Unlock()
-	return x.actors.pids()
+	actors := make([]PID, 0, len(pids))
+	for _, pid := range pids {
+		if !isSystemName(pid.Name()) {
+			actors = append(actors, pid)
+		}
+	}
+	return actors
 }
 
 // PeerAddress returns the actor system address known in the cluster. That address is used by other nodes to communicate with the actor system.
@@ -698,6 +736,15 @@ func (x *actorSystem) Start(ctx context.Context) error {
 
 	x.scheduler.Start(spanCtx)
 
+	actorName := x.getSystemActorName(supervisorType)
+	pid, err := x.configPID(ctx, actorName, newSystemSupervisor(x.logger))
+	if err != nil {
+		return fmt.Errorf("actor=%s failed to start system supervisor: %w", actorName, err)
+	}
+
+	x.supervisor = pid
+	x.setActor(pid)
+
 	go x.gc()
 
 	x.logger.Infof("%s started..:)", x.name)
@@ -709,6 +756,8 @@ func (x *actorSystem) Stop(ctx context.Context) error {
 	spanCtx, span := x.tracer.Start(ctx, "Stop")
 	defer span.End()
 
+	x.logger.Infof("%s shutting down...", x.name)
+
 	// make sure the actor system has started
 	if !x.started.Load() {
 		e := ErrActorSystemNotStarted
@@ -753,10 +802,13 @@ func (x *actorSystem) Stop(ctx context.Context) error {
 		close(x.redistributionChan)
 	}
 
-	if len(x.Actors()) == 0 {
-		x.logger.Info("No online actors to shutdown. Shutting down successfully done")
-		return nil
+	// stop the supervisor actor
+	if err := x.getSupervisor().Shutdown(ctx); err != nil {
+		x.reset()
+		return err
 	}
+	// remove the supervisor from the actors list
+	x.actors.delete(x.supervisor.ActorPath())
 
 	for _, actor := range x.Actors() {
 		x.actors.delete(actor.ActorPath())
@@ -766,8 +818,8 @@ func (x *actorSystem) Stop(ctx context.Context) error {
 		}
 	}
 
-	x.actors.close()
 	x.reset()
+	x.logger.Infof("%s shuts down successfully", x.name)
 	return nil
 }
 
@@ -1086,6 +1138,14 @@ func (x *actorSystem) handleRemoteTell(ctx context.Context, to PID, message prot
 	return Tell(spanCtx, to, message)
 }
 
+// getSupervisor return the system supervisor
+func (x *actorSystem) getSupervisor() PID {
+	x.locker.Lock()
+	supervisor := x.supervisor
+	x.locker.Unlock()
+	return supervisor
+}
+
 // setActor implements ActorSystem.
 func (x *actorSystem) setActor(actor PID) {
 	x.actors.set(actor)
@@ -1456,7 +1516,6 @@ func (x *actorSystem) configPID(ctx context.Context, name string, actor Actor) (
 	// pid inherit the actor system settings defined during instantiation
 	pidOpts := []pidOption{
 		withInitMaxRetries(x.actorInitMaxRetries),
-		withPassivationAfter(x.expireActorAfter),
 		withAskTimeout(x.askTimeout),
 		withCustomLogger(x.logger),
 		withActorSystem(x),
@@ -1469,6 +1528,13 @@ func (x *actorSystem) configPID(ctx context.Context, name string, actor Actor) (
 		withTelemetry(x.telemetry),
 	}
 
+	// disable passivation for system supervisor
+	if isSystemName(name) {
+		pidOpts = append(pidOpts, withPassivationDisabled())
+	} else {
+		pidOpts = append(pidOpts, withPassivationAfter(x.expireActorAfter))
+	}
+
 	if x.traceEnabled.Load() {
 		pidOpts = append(pidOpts, withTracing())
 	}
@@ -1487,3 +1553,23 @@ func (x *actorSystem) configPID(ctx context.Context, name string, actor Actor) (
 	}
 	return pid, nil
 }
+
+// getSystemActorName returns the system supervisor name
+func (x *actorSystem) getSystemActorName(nameType nameType) string {
+	if x.remotingEnabled.Load() {
+		return fmt.Sprintf("%s%s%s-%d-%d",
+			systemNames[nameType],
+			strings.ToTitle(x.name),
+			x.remotingHost,
+			x.remotingPort,
+			time.Now().UnixNano())
+	}
+	return fmt.Sprintf("%s%s-%d",
+		systemNames[nameType],
+		strings.ToTitle(x.name),
+		time.Now().UnixNano())
+}
+
+func isSystemName(name string) bool {
+	return strings.HasPrefix(name, systemNamePrefix)
+}
diff --git a/actors/errors.go b/actors/errors.go
index b71c9ed..682dc3b 100644
--- a/actors/errors.go
+++ b/actors/errors.go
@@ -25,11 +25,11 @@
 package actors
 
 import (
+	"errors"
 	"fmt"
 	"io"
 
 	"connectrpc.com/connect"
-	"github.com/pkg/errors"
 )
 
 var (
@@ -62,11 +62,11 @@ var (
 	// ErrMethodCallNotAllowed is returned when rpc call is not allowed
 	ErrMethodCallNotAllowed = errors.New("method call not allowed")
 	// ErrInvalidRemoteMessage is returned when an invalid remote message is sent
-	ErrInvalidRemoteMessage = func(err error) error { return errors.Wrap(err, "invalid remote message") }
+	ErrInvalidRemoteMessage = func(err error) error { return fmt.Errorf("invalid remote message: %w", err) }
 	// ErrStashBufferNotSet when stashing is not set while requesting for messages to be stashed
 	ErrStashBufferNotSet = errors.New("actor is not setup with a stash buffer")
 	// ErrInitFailure is returned when the initialization of an actor fails.
-	ErrInitFailure = func(err error) error { return errors.Wrap(err, "failed to initialize") }
+	ErrInitFailure = func(err error) error { return fmt.Errorf("failed to initialize: %w", err) }
 	// ErrActorSystemNotStarted is returned when the actor is not started while accessing the features of the actor system
 	ErrActorSystemNotStarted = errors.New("actor system has not started yet")
 	// ErrLocalAddress is returned when a remote address is used instead of a local address
@@ -87,6 +87,8 @@ var (
 	ErrUndefinedTask = errors.New("task is not defined")
 	// ErrInvalidHost is returned when a request is sent to an invalid host
 	ErrInvalidHost = errors.New("invalid host")
+	// ErrInvalidPath is returned when an actor path is invalid
+	ErrInvalidPath = func(err error) error { return fmt.Errorf("invalid path: %w", err) }
 )
 
 // IsEOF returns true if the given error is an EOF error
diff --git a/actors/option.go b/actors/option.go
index bedaf8d..59df6f8 100644
--- a/actors/option.go
+++ b/actors/option.go
@@ -87,7 +87,7 @@ func WithPassivationDisabled() Option {
 }
 
 // WithSupervisorDirective sets the supervisor strategy directive
-func WithSupervisorDirective(directive supervisorDirective) Option {
+func WithSupervisorDirective(directive SupervisorDirective) Option {
 	return OptionFunc(func(a *actorSystem) {
 		a.supervisorDirective = directive
 	})
diff --git a/actors/path.go b/actors/path.go
index e50b6b4..09a565a 100644
--- a/actors/path.go
+++ b/actors/path.go
@@ -26,10 +26,13 @@ package actors
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/google/uuid"
+	"github.com/pkg/errors"
 
 	"github.com/tochemey/goakt/v2/goaktpb"
+	"github.com/tochemey/goakt/v2/internal/validation"
 )
 
 // Path is a unique path to an actor
@@ -44,6 +47,8 @@ type Path struct {
 	parent *Path
 }
 
+var _ validation.Validator = (*Path)(nil)
+
 // NewPath creates an immutable Path
 func NewPath(name string, address *Address) *Path {
 	// create the instance and return it
@@ -105,3 +110,14 @@ func (p *Path) RemoteAddress() *goaktpb.Address {
 		Id:   p.ID().String(),
 	}
 }
+
+// Validate returns an error when the path is not valid
+func (p *Path) Validate() error {
+	pattern := "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$"
+	customErr := errors.New("path name must contain only word characters (i.e. [a-zA-Z0-9] plus non-leading '-' or '_')")
+	return validation.
+		New(validation.FailFast()).
+		AddValidator(validation.NewPatternValidator(pattern, strings.TrimSpace(p.name), customErr)).
+		AddAssertion(p.address != nil, "address is required").
+		Validate()
+}
diff --git a/actors/pid.go b/actors/pid.go
index 154b4cb..2790d55 100644
--- a/actors/pid.go
+++ b/actors/pid.go
@@ -26,6 +26,7 @@ package actors
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	stdhttp "net/http"
 	"strings"
@@ -35,7 +36,6 @@ import (
 	"connectrpc.com/connect"
 	"connectrpc.com/otelconnect"
 	"github.com/flowchartsman/retry"
-	"github.com/pkg/errors"
 	"go.opentelemetry.io/otel/codes"
 	otelmetric "go.opentelemetry.io/otel/metric"
 	"go.opentelemetry.io/otel/trace"
@@ -137,9 +137,12 @@ type PID interface {
 	RemoteStop(ctx context.Context, host string, port int, name string) error
 	// RemoteSpawn creates an actor on a remote node. The given actor needs to be registered on the remote node using the Register method of ActorSystem
 	RemoteSpawn(ctx context.Context, host string, port int, name, actorType string) error
-	// Children returns the list of all the children of the given actor that are still alive
-	// or an empty list.
+	// Children returns the list of all the direct children of the given actor
+	// Only alive actors are included in the list or an empty list is returned
 	Children() []PID
+	// Parents returns the list of all direct parents of a given actor.
+	// Only alive actors are included in the list or an empty list is returned
+	Parents() []PID
 	// Child returns the named child actor if it is alive
 	Child(name string) (PID, error)
 	// Stop forces the child Actor under the given name to terminate after it finishes processing its current message.
@@ -156,8 +159,10 @@ type PID interface {
 	PipeTo(ctx context.Context, to PID, task future.Task) error
 	// push a message to the actor's receiveContextBuffer
 	doReceive(ctx ReceiveContext)
-	// watchers returns the list of watchMen
-	watchers() *slices.ThreadSafe[*watcher]
+	// watchers returns the list of actors watching this actor
+	watchers() *slices.Safe[*watcher]
+	// watchees returns the list of actors watched by this actor
+	watchees() *pidMap
 	// setBehavior is a utility function that helps set the actor behavior
 	setBehavior(behavior Behavior)
 	// setBehaviorStacked adds a behavior to the actor's behaviors
@@ -226,12 +231,15 @@ type pid struct {
 	shutdownSignal     chan types.Unit
 	haltPassivationLnr chan types.Unit
 
-	// set of watchersList watching the given actor
-	watchersList *slices.ThreadSafe[*watcher]
+	// hold the watchersList watching the given actor
+	watchersList *slices.Safe[*watcher]
 
 	// hold the list of the children
 	children *pidMap
 
+	// hold the list of watched actors
+	watchedList *pidMap
+
 	// the actor system
 	system ActorSystem
 
@@ -248,8 +256,8 @@ type pid struct {
 	stopLocker           *sync.Mutex
 	processingTimeLocker *sync.Mutex
 
-	// supervisor strategy
-	supervisorDirective supervisorDirective
+	// testSupervisor strategy
+	supervisorDirective SupervisorDirective
 
 	// observability settings
 	telemetry *telemetry.Telemetry
@@ -283,6 +291,16 @@ var _ PID = (*pid)(nil)
 
 // newPID creates a new pid
 func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption) (*pid, error) {
+	// actor path is required
+	if actorPath == nil {
+		return nil, errors.New("actorPath is required")
+	}
+
+	// validate actor path
+	if err := actorPath.Validate(); err != nil {
+		return nil, err
+	}
+
 	p := &pid{
 		Actor:                actor,
 		lastProcessingTime:   atomic.Time{},
@@ -292,7 +310,8 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 		mailboxSize:          DefaultMailboxSize,
 		children:             newPIDMap(10),
 		supervisorDirective:  DefaultSupervisoryStrategy,
-		watchersList:         slices.NewThreadSafe[*watcher](),
+		watchersList:         slices.NewSafe[*watcher](),
+		watchedList:          newPIDMap(10),
 		telemetry:            telemetry.New(),
 		actorPath:            actorPath,
 		rwLocker:             &sync.RWMutex{},
@@ -351,7 +370,7 @@ func newPID(ctx context.Context, actorPath *Path, actor Actor, opts ...pidOption
 
 	if p.metricEnabled.Load() {
 		if err := p.registerMetrics(); err != nil {
-			return nil, errors.Wrapf(err, "failed to register actor=%s metrics", p.ActorPath().String())
+			return nil, fmt.Errorf("failed to register actor=%s metrics: %w", p.ID(), err)
 		}
 	}
 
@@ -394,7 +413,29 @@ func (x *pid) Child(name string) (PID, error) {
 	return nil, ErrActorNotFound(childActorPath.String())
 }
 
-// Children returns the list of all the children of the given actor that are still alive or an empty list
+// Parents returns the list of all direct parents of a given actor.
+// Only alive actors are included in the list or an empty list is returned
+func (x *pid) Parents() []PID {
+	x.rwLocker.Lock()
+	watchers := x.watchersList
+	x.rwLocker.Unlock()
+	var parents []PID
+	if watchers.Len() > 0 {
+		for item := range watchers.Iter() {
+			watcher := item.Value
+			if watcher != nil {
+				pid := watcher.WatcherID
+				if pid.IsRunning() {
+					parents = append(parents, pid)
+				}
+			}
+		}
+	}
+	return parents
+}
+
+// Children returns the list of all the direct children of the given actor
+// Only alive actors are included in the list or an empty list is returned
 func (x *pid) Children() []PID {
 	x.rwLocker.RLock()
 	children := x.children.pids()
@@ -832,13 +873,15 @@ func (x *pid) RemoteTell(ctx context.Context, to *goaktpb.Address, message proto
 			}
 			return nil
 		}
-		x.logger.Error(errors.Wrapf(err, "failed to send message to remote=(%s:%d)", to.GetHost(), to.GetPort()))
-		return err
+		fmtErr := fmt.Errorf("failed to send message to remote=(%s:%d): %w", to.GetHost(), to.GetPort(), err)
+		x.logger.Error(fmtErr)
+		return fmtErr
 	}
 
 	if _, err := stream.CloseAndReceive(); err != nil {
-		x.logger.Error(errors.Wrapf(err, "failed to send message to remote=(%s:%d)", to.GetHost(), to.GetPort()))
-		return err
+		fmtErr := fmt.Errorf("failed to send message to remote=(%s:%d): %w", to.GetHost(), to.GetPort(), err)
+		x.logger.Error(fmtErr)
+		return fmtErr
 	}
 
 	x.logger.Debugf("message successfully sent to remote=(%s:%d)", to.GetHost(), to.GetPort())
@@ -1147,7 +1190,7 @@ func (x *pid) Shutdown(ctx context.Context) error {
 	}
 
 	if err := x.doStop(spanCtx); err != nil {
-		x.logger.Errorf("failed to stop actor=(%s)", x.ActorPath().String())
+		x.logger.Errorf("failed to cleanly stop actor=(%s)", x.ID())
 		return err
 	}
 
@@ -1158,7 +1201,7 @@ func (x *pid) Shutdown(ctx context.Context) error {
 		})
 	}
 
-	x.logger.Infof("Actor=%s successfully shutdown", x.ActorPath().String())
+	x.logger.Infof("Actor=%s successfully shutdown", x.ID())
 	span.SetStatus(codes.Ok, "Shutdown")
 	return nil
 }
@@ -1171,6 +1214,7 @@ func (x *pid) Watch(cid PID) {
 		Done:      make(chan types.Unit, 1),
 	}
 	cid.watchers().Append(w)
+	x.watchees().set(cid)
 	go x.supervise(cid, w)
 }
 
@@ -1178,19 +1222,25 @@ func (x *pid) Watch(cid PID) {
 func (x *pid) UnWatch(pid PID) {
 	for item := range pid.watchers().Iter() {
 		w := item.Value
-		if w.WatcherID.ActorPath().Equals(x.ActorPath()) {
+		if w.WatcherID.Equals(x) {
 			w.Done <- types.Unit{}
+			x.watchees().delete(pid.ActorPath())
 			pid.watchers().Delete(item.Index)
 			break
 		}
 	}
 }
 
-// Watchers return the list of watchersList
-func (x *pid) watchers() *slices.ThreadSafe[*watcher] {
+// watchers return the list of watchersList
+func (x *pid) watchers() *slices.Safe[*watcher] {
 	return x.watchersList
 }
 
+// watchees returns the list of actors watched by this actor
+func (x *pid) watchees() *pidMap {
+	return x.watchedList
+}
+
 // doReceive pushes a given message to the actor receiveContextBuffer
 func (x *pid) doReceive(ctx ReceiveContext) {
 	x.rwLocker.Lock()
@@ -1252,20 +1302,20 @@ func (x *pid) reset() {
 	x.lastProcessingDuration.Store(0)
 	x.initTimeout.Store(DefaultInitTimeout)
 	x.children = newPIDMap(10)
-	x.watchersList = slices.NewThreadSafe[*watcher]()
+	x.watchersList = slices.NewSafe[*watcher]()
 	x.telemetry = telemetry.New()
 	x.mailbox.Reset()
 	x.resetBehavior()
 	if x.metricEnabled.Load() {
 		if err := x.registerMetrics(); err != nil {
-			x.logger.Error(errors.Wrapf(err, "failed to register actor=%s metrics", x.ActorPath().String()))
+			fmtErr := fmt.Errorf("failed to register actor=%s metrics: %w", x.ID(), err)
+			x.logger.Error(fmtErr)
 		}
 	}
 }
 
 func (x *pid) freeWatchers(ctx context.Context) {
 	x.logger.Debug("freeing all watcher actors...")
-
 	watchers := x.watchers()
 	if watchers.Len() > 0 {
 		for item := range watchers.Iter() {
@@ -1274,24 +1324,44 @@ func (x *pid) freeWatchers(ctx context.Context) {
 				ActorId: x.ID(),
 			}
 			if watcher.WatcherID.IsRunning() {
+				x.logger.Debugf("watcher=(%s) releasing watched=(%s)", watcher.WatcherID.ID(), x.ID())
 				// TODO: handle error and push to some system dead-letters queue
 				_ = x.Tell(ctx, watcher.WatcherID, terminated)
 				watcher.WatcherID.UnWatch(x)
 				watcher.WatcherID.removeChild(x)
+				x.logger.Debugf("watcher=(%s) released watched=(%s)", watcher.WatcherID.ID(), x.ID())
 			}
 		}
 	}
 }
 
+func (x *pid) freeWatchees(ctx context.Context) {
+	x.logger.Debug("freeing all watched actors...")
+	for _, watched := range x.watchedList.pids() {
+		x.logger.Debugf("watcher=(%s) unwatching actor=(%s)", x.ID(), watched.ID())
+		x.UnWatch(watched)
+		if err := watched.Shutdown(ctx); err != nil {
+			x.logger.Panic(
+				fmt.Errorf("watcher=(%s) failed to unwatch actor=(%s): %w",
+					x.ID(), watched.ID(), err))
+		}
+		x.logger.Debugf("watcher=(%s) successfully unwatch actor=(%s)", x.ID(), watched.ID())
+	}
+}
+
 func (x *pid) freeChildren(ctx context.Context) {
 	x.logger.Debug("freeing all child actors...")
-
 	for _, child := range x.Children() {
+		x.logger.Debugf("parent=(%s) disowning child=(%s)", x.ID(), child.ID())
 		x.UnWatch(child)
 		x.children.delete(child.ActorPath())
 		if err := child.Shutdown(ctx); err != nil {
-			x.logger.Panic(err)
+			x.logger.Panic(
+				fmt.Errorf(
+					"parent=(%s) failed to disown child=(%s): %w", x.ID(), child.ID(),
+					err))
 		}
+		x.logger.Debugf("parent=(%s) successfully disown child=(%s)", x.ID(), child.ID())
 	}
 }
 
@@ -1430,6 +1500,7 @@ func (x *pid) doStop(ctx context.Context) error {
 	// TODO: just signal stash processing done and ignore the messages or process them
 	for x.stashBuffer != nil && !x.stashBuffer.IsEmpty() {
 		if err := x.unstashAll(); err != nil {
+			x.logger.Errorf("actor=(%s) failed to unstash messages", x.ActorPath().String())
 			span.SetStatus(codes.Error, "doStop")
 			span.RecordError(err)
 			return err
@@ -1461,6 +1532,8 @@ func (x *pid) doStop(ctx context.Context) error {
 	<-tickerStopSig
 	x.shutdownSignal <- types.Unit{}
 	x.httpClient.CloseIdleConnections()
+
+	x.freeWatchees(spanCtx)
 	x.freeChildren(spanCtx)
 	x.freeWatchers(spanCtx)
 
@@ -1549,7 +1622,7 @@ func (x *pid) getConnectionOptions() ([]connect.ClientOption, error) {
 			otelconnect.WithTracerProvider(x.telemetry.TracerProvider),
 			otelconnect.WithMeterProvider(x.telemetry.MeterProvider))
 		if err != nil {
-			return nil, errors.Wrap(err, "failed to initialize observability feature")
+			return nil, fmt.Errorf("failed to initialize observability feature: %w", err)
 		}
 	}
 
@@ -1640,3 +1713,63 @@ func (x *pid) handleCompletion(ctx context.Context, completion *completion) {
 	to.doReceive(messageContext)
 	span.SetStatus(codes.Ok, "TaskCompletion")
 }
+
+// supervise watches for child actor's failure and act based upon the supervisory strategy
+func (x *pid) supervise(cid PID, watcher *watcher) {
+	for {
+		select {
+		case <-watcher.Done:
+			x.logger.Debugf("stop watching cid=(%s)", cid.ID())
+			return
+		case err := <-watcher.ErrChan:
+			x.logger.Errorf("child actor=(%s) is failing: Err=%v", cid.ID(), err)
+			switch directive := x.supervisorDirective.(type) {
+			case *StopDirective:
+				x.handleStopDirective(cid)
+			case *RestartDirective:
+				x.handleRestartDirective(cid, directive.MaxNumRetries(), directive.Timeout())
+			case *ResumeDirective:
+				// pass
+			default:
+				x.handleStopDirective(cid)
+			}
+		}
+	}
+}
+
+// handleStopDirective handles the testSupervisor stop directive
+func (x *pid) handleStopDirective(cid PID) {
+	x.UnWatch(cid)
+	x.children.delete(cid.ActorPath())
+	if err := cid.Shutdown(context.Background()); err != nil {
+		// this can enter into some infinite loop if we panic
+		// since we are just shutting down the actor we can just log the error
+		// TODO: rethink properly about PostStop error handling
+		x.logger.Error(err)
+	}
+}
+
+// handleRestartDirective handles the testSupervisor restart directive
+func (x *pid) handleRestartDirective(cid PID, maxRetries uint32, timeout time.Duration) {
+	x.UnWatch(cid)
+	ctx := context.Background()
+	var err error
+	if maxRetries == 0 || timeout <= 0 {
+		err = cid.Restart(ctx)
+	} else {
+		// TODO: handle the initial delay
+		retrier := retry.NewRetrier(int(maxRetries), 100*time.Millisecond, timeout)
+		err = retrier.RunContext(ctx, cid.Restart)
+	}
+
+	if err != nil {
+		x.logger.Error(err)
+		// remove the actor and stop it
+		x.children.delete(cid.ActorPath())
+		if err := cid.Shutdown(ctx); err != nil {
+			x.logger.Error(err)
+		}
+		return
+	}
+	x.Watch(cid)
+}
diff --git a/actors/pid_map.go b/actors/pid_map.go
index 0086d8e..acfa99a 100644
--- a/actors/pid_map.go
+++ b/actors/pid_map.go
@@ -25,28 +25,20 @@
 package actors
 
 import (
-	"reflect"
 	"sync"
 
 	"go.uber.org/atomic"
-
-	"github.com/tochemey/goakt/v2/internal/types"
 )
 
-type prop struct {
-	pid   PID
-	rtype reflect.Type
-}
-
 type pidMap struct {
 	mu       *sync.RWMutex
 	size     atomic.Int32
-	mappings map[string]*prop
+	mappings map[string]PID
 }
 
 func newPIDMap(cap int) *pidMap {
 	return &pidMap{
-		mappings: make(map[string]*prop, cap),
+		mappings: make(map[string]PID, cap),
 		mu:       &sync.RWMutex{},
 	}
 }
@@ -59,12 +51,8 @@ func (m *pidMap) len() int {
 // get retrieves a pid by its address
 func (m *pidMap) get(path *Path) (pid PID, ok bool) {
 	m.mu.RLock()
-	defer m.mu.RUnlock()
-	prop, ok := m.mappings[path.String()]
-	if prop != nil {
-		pid = prop.pid
-		ok = true
-	}
+	pid, ok = m.mappings[path.String()]
+	m.mu.RUnlock()
 	return
 }
 
@@ -73,16 +61,7 @@ func (m *pidMap) set(pid PID) {
 	m.mu.Lock()
 	defer m.mu.Unlock()
 	if pid != nil {
-		var rtype reflect.Type
-		handle := pid.ActorHandle()
-		if handle != nil {
-			rtype = types.Of(handle)
-		}
-
-		m.mappings[pid.ActorPath().String()] = &prop{
-			pid:   pid,
-			rtype: rtype,
-		}
+		m.mappings[pid.ActorPath().String()] = pid
 		m.size.Add(1)
 	}
 }
@@ -100,21 +79,14 @@ func (m *pidMap) pids() []PID {
 	m.mu.Lock()
 	var out []PID
 	for _, prop := range m.mappings {
-		out = append(out, prop.pid)
+		out = append(out, prop)
 	}
 	m.mu.Unlock()
 	return out
 }
 
-func (m *pidMap) props() map[string]*prop {
-	m.mu.Lock()
-	out := m.mappings
-	m.mu.Unlock()
-	return out
-}
-
 func (m *pidMap) close() {
 	m.mu.Lock()
-	m.mappings = make(map[string]*prop)
+	m.mappings = make(map[string]PID)
 	m.mu.Unlock()
 }
diff --git a/actors/pid_option.go b/actors/pid_option.go
index f95122c..85389e1 100644
--- a/actors/pid_option.go
+++ b/actors/pid_option.go
@@ -73,7 +73,7 @@ func withActorSystem(sys ActorSystem) pidOption {
 
 // withSupervisorDirective sets the supervisor strategy to used when dealing
 // with child actors
-func withSupervisorDirective(directive supervisorDirective) pidOption {
+func withSupervisorDirective(directive SupervisorDirective) pidOption {
 	return func(pid *pid) {
 		pid.supervisorDirective = directive
 	}
diff --git a/actors/router.go b/actors/router.go
new file mode 100644
index 0000000..f0b82eb
--- /dev/null
+++ b/actors/router.go
@@ -0,0 +1,200 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2024 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package actors
+
+import (
+	"context"
+	"fmt"
+	"math/rand/v2"
+	"reflect"
+	"sync/atomic"
+
+	"github.com/tochemey/goakt/v2/goaktpb"
+	"github.com/tochemey/goakt/v2/log"
+)
+
+// RouterOption is the interface that applies a configuration option.
+type RouterOption interface {
+	// Apply sets the Option value of a config.
+	Apply(cl *router)
+}
+
+// enforce compilation error
+var _ RouterOption = RouterOptionFunc(nil)
+
+// RouterOptionFunc implements the Option interface.
+type RouterOptionFunc func(router *router)
+
+func (f RouterOptionFunc) Apply(c *router) {
+	f(c)
+}
+
+// WithRoutingStrategy sets the routing strategy
+func WithRoutingStrategy(strategy RoutingStrategy) RouterOption {
+	return RouterOptionFunc(func(r *router) {
+		r.strategy = strategy
+	})
+}
+
+// RoutingStrategy defines the routing strategy to use when
+// defining routers
+type RoutingStrategy int
+
+const (
+	RoundRobinRouting RoutingStrategy = iota
+	RandomRouting
+	FanOutRouting
+)
+
+// router is an actor that depending upon the routing
+// strategy route message to its routees.
+type router struct {
+	strategy RoutingStrategy
+	poolSize int
+	// list of routees
+	routeesMap  map[string]PID
+	next        uint32
+	routeesKind reflect.Type
+	logger      log.Logger
+}
+
+var _ Actor = (*router)(nil)
+
+// newRouter creates an instance of router giving the routing strategy and poolSize
+// The poolSize specifies the number of routees to spawn by the router
+func newRouter(poolSize int, routeesKind Actor, loggger log.Logger, opts ...RouterOption) *router {
+	router := &router{
+		strategy:    FanOutRouting,
+		poolSize:    poolSize,
+		routeesMap:  make(map[string]PID, poolSize),
+		routeesKind: reflect.TypeOf(routeesKind).Elem(),
+		logger:      loggger,
+	}
+
+	// apply the various options
+	for _, opt := range opts {
+		opt.Apply(router)
+	}
+	return router
+}
+
+// PreStart pre-starts the actor.
+func (x *router) PreStart(context.Context) error {
+	x.logger.Info("starting the router...")
+	return nil
+}
+
+// Receive handles messages sent to the router
+func (x *router) Receive(ctx ReceiveContext) {
+	message := ctx.Message()
+	switch message.(type) {
+	case *goaktpb.PostStart:
+		x.postStart(ctx)
+	default:
+		ctx.Unhandled()
+	}
+}
+
+// PostStop is executed when the actor is shutting down.
+func (x *router) PostStop(context.Context) error {
+	x.logger.Info("router stopped")
+	return nil
+}
+
+// postStart spawns routeesMap
+func (x *router) postStart(ctx ReceiveContext) {
+	x.logger.Info("router successfully started")
+	for i := 0; i < x.poolSize; i++ {
+		routeeName := routeeName(ctx.Self().Name(), i)
+		actor := reflect.New(x.routeesKind).Interface().(Actor)
+		routee := ctx.Spawn(routeeName, actor)
+		x.routeesMap[routee.ID()] = routee
+	}
+	ctx.Become(x.broadcast)
+}
+
+// broadcast send message to all the routeesMap
+func (x *router) broadcast(ctx ReceiveContext) {
+	var message *goaktpb.Broadcast
+	switch msg := ctx.Message().(type) {
+	case *goaktpb.Broadcast:
+		message = msg
+	case *goaktpb.Terminated:
+		delete(x.routeesMap, msg.GetActorId())
+		return
+	default:
+		ctx.Unhandled()
+		return
+	}
+
+	routees, proceed := x.availableRoutees()
+	if !proceed {
+		x.logger.Warn("no routees available. stopping.... Bye")
+		// push message to deadletters
+		ctx.Unhandled()
+		// shutdown
+		ctx.Shutdown()
+		return
+	}
+
+	msg, err := message.GetMessage().UnmarshalNew()
+	if err != nil {
+		ctx.Err(err)
+		return
+	}
+
+	switch x.strategy {
+	case RoundRobinRouting:
+		n := atomic.AddUint32(&x.next, 1)
+		routee := routees[(int(n)-1)%len(routees)]
+		ctx.Tell(routee, msg)
+	case RandomRouting:
+		routee := routees[rand.IntN(len(routees))]
+		ctx.Tell(routee, msg)
+	default:
+		for _, routee := range routees {
+			routee := routee
+			go func(pid PID) {
+				ctx.Tell(pid, msg)
+			}(routee)
+		}
+	}
+}
+
+// routeeName returns the routee name
+func routeeName(routerName string, routeeIndex int) string {
+	return fmt.Sprintf("%s-%s-%d", routeeNamePrefix, routerName, routeeIndex)
+}
+
+func (x *router) availableRoutees() ([]PID, bool) {
+	routees := make([]PID, 0, x.poolSize)
+	for _, routee := range x.routeesMap {
+		if !routee.IsRunning() {
+			delete(x.routeesMap, routee.ID())
+		}
+		routees = append(routees, routee)
+	}
+	return routees, len(routees) > 0
+}
diff --git a/actors/supervisor.go b/actors/supervisor_directive.go
similarity index 54%
rename from actors/supervisor.go
rename to actors/supervisor_directive.go
index b801fc7..e11d70f 100644
--- a/actors/supervisor.go
+++ b/actors/supervisor_directive.go
@@ -24,19 +24,14 @@
 
 package actors
 
-import (
-	"context"
-	"time"
+import "time"
 
-	"github.com/flowchartsman/retry"
-)
-
-// supervisorDirective defines the supervisor directive
-type supervisorDirective interface {
+// SupervisorDirective defines the testSupervisor directive
+type SupervisorDirective interface {
 	isSupervisorDirective()
 }
 
-// StopDirective defines the supervisor stop directive
+// StopDirective defines the testSupervisor stop directive
 type StopDirective struct{}
 
 // NewStopDirective creates an instance of StopDirective
@@ -46,7 +41,7 @@ func NewStopDirective() *StopDirective {
 
 func (*StopDirective) isSupervisorDirective() {}
 
-// ResumeDirective defines the supervisor resume directive
+// ResumeDirective defines the testSupervisor resume directive
 // This ignores the failure and process the next message, instead
 type ResumeDirective struct{}
 
@@ -57,7 +52,7 @@ func NewResumeDirective() *ResumeDirective {
 
 func (*ResumeDirective) isSupervisorDirective() {}
 
-// RestartDirective defines supervisor restart directive
+// RestartDirective defines testSupervisor restart directive
 type RestartDirective struct {
 	// Specifies the maximum number of retries
 	// When reaching this number the faulty actor is stopped
@@ -91,63 +86,3 @@ func (x *RestartDirective) WithLimit(maxNumRetries uint32, timeout time.Duration
 }
 
 func (*RestartDirective) isSupervisorDirective() {}
-
-// supervise watches for child actor's failure and act based upon the supervisory strategy
-func (x *pid) supervise(cid PID, watcher *watcher) {
-	for {
-		select {
-		case <-watcher.Done:
-			x.logger.Debugf("stop watching cid=(%s)", cid.ActorPath().String())
-			return
-		case err := <-watcher.ErrChan:
-			x.logger.Errorf("child actor=(%s) is failing: Err=%v", cid.ActorPath().String(), err)
-			switch directive := x.supervisorDirective.(type) {
-			case *StopDirective:
-				x.handleStopDirective(cid)
-			case *RestartDirective:
-				x.handleRestartDirective(cid, directive.MaxNumRetries(), directive.Timeout())
-			case *ResumeDirective:
-				// pass
-			default:
-				x.handleStopDirective(cid)
-			}
-		}
-	}
-}
-
-// handleStopDirective handles the supervisor stop directive
-func (x *pid) handleStopDirective(cid PID) {
-	x.UnWatch(cid)
-	x.children.delete(cid.ActorPath())
-	if err := cid.Shutdown(context.Background()); err != nil {
-		// this can enter into some infinite loop if we panic
-		// since we are just shutting down the actor we can just log the error
-		// TODO: rethink properly about PostStop error handling
-		x.logger.Error(err)
-	}
-}
-
-// handleRestartDirective handles the supervisor restart directive
-func (x *pid) handleRestartDirective(cid PID, maxRetries uint32, timeout time.Duration) {
-	x.UnWatch(cid)
-	ctx := context.Background()
-	var err error
-	if maxRetries == 0 || timeout <= 0 {
-		err = cid.Restart(ctx)
-	} else {
-		// TODO: handle the initial delay
-		retrier := retry.NewRetrier(int(maxRetries), 100*time.Millisecond, timeout)
-		err = retrier.RunContext(ctx, cid.Restart)
-	}
-
-	if err != nil {
-		x.logger.Error(err)
-		// remove the actor and stop it
-		x.children.delete(cid.ActorPath())
-		if err := cid.Shutdown(ctx); err != nil {
-			x.logger.Error(err)
-		}
-		return
-	}
-	x.Watch(cid)
-}
diff --git a/actors/types.go b/actors/types.go
index 4d2c460..cce92eb 100644
--- a/actors/types.go
+++ b/actors/types.go
@@ -55,6 +55,16 @@ const (
 	DefaultGCInterval = 30 * time.Millisecond
 	// eventsTopic defines the events topic
 	eventsTopic = "topic.events"
+
+	systemNamePrefix = "GoAkt"
+	routeeNamePrefix = "GoAktRoutee"
+)
+
+type nameType int
+
+const (
+	supervisorType nameType = iota
+	routerType
 )
 
 var (
@@ -64,4 +74,9 @@ var (
 	DefaultSupervisoryStrategy = NewStopDirective()
 	// RemoteNoSender means that there is no sender
 	RemoteNoSender = new(goaktpb.Address)
+
+	systemNames = map[nameType]string{
+		supervisorType: "GoAktSupervisor",
+		routerType:     "GoAktRouter",
+	}
 )
diff --git a/internal/cluster/engine.go b/internal/cluster/engine.go
index 85c6aee..d4c75a2 100644
--- a/internal/cluster/engine.go
+++ b/internal/cluster/engine.go
@@ -506,11 +506,11 @@ func (n *Engine) RemoveActor(ctx context.Context, actorName string) error {
 	n.Lock()
 	defer n.Unlock()
 
-	logger.Infof("removing actor (%s)", actorName)
+	logger.Infof("removing actor (%s) from cluster", actorName)
 
 	_, err := n.dmap.Delete(ctx, actorName)
 	if err != nil {
-		logger.Error(errors.Wrapf(err, "failed to remove actor=%s record", actorName))
+		logger.Error(errors.Wrapf(err, "failed to remove actor=%s record from the cluster", actorName))
 		return err
 	}
 
diff --git a/internal/slices/slices.go b/internal/slices/slices.go
index ff02152..ac5ea7b 100644
--- a/internal/slices/slices.go
+++ b/internal/slices/slices.go
@@ -26,8 +26,8 @@ package slices
 
 import "sync"
 
-// ThreadSafe type that can be safely shared between goroutines.
-type ThreadSafe[T any] struct {
+// Safe type that can be safely shared between goroutines.
+type Safe[T any] struct {
 	sync.RWMutex
 	items []T
 }
@@ -38,9 +38,9 @@ type Item[T any] struct {
 	Value T
 }
 
-// NewThreadSafe creates a new synchronized slice.
-func NewThreadSafe[T any]() *ThreadSafe[T] {
-	cs := &ThreadSafe[T]{
+// NewSafe creates a new synchronized slice.
+func NewSafe[T any]() *Safe[T] {
+	cs := &Safe[T]{
 		items: make([]T, 0),
 	}
 
@@ -48,21 +48,21 @@ func NewThreadSafe[T any]() *ThreadSafe[T] {
 }
 
 // Len returns the number of items
-func (cs *ThreadSafe[T]) Len() int {
+func (cs *Safe[T]) Len() int {
 	cs.Lock()
 	defer cs.Unlock()
 	return len(cs.items)
 }
 
 // Append adds an item to the concurrent slice.
-func (cs *ThreadSafe[T]) Append(item T) {
+func (cs *Safe[T]) Append(item T) {
 	cs.Lock()
 	defer cs.Unlock()
 	cs.items = append(cs.items, item)
 }
 
 // Get returns the slice item at the given index
-func (cs *ThreadSafe[T]) Get(index int) (item any) {
+func (cs *Safe[T]) Get(index int) (item any) {
 	cs.RLock()
 	defer cs.RUnlock()
 	if isSet(cs.items, index) {
@@ -72,7 +72,7 @@ func (cs *ThreadSafe[T]) Get(index int) (item any) {
 }
 
 // Delete an item from the slice
-func (cs *ThreadSafe[T]) Delete(index int) {
+func (cs *Safe[T]) Delete(index int) {
 	cs.RLock()
 	defer cs.RUnlock()
 	var nilState T
@@ -91,7 +91,7 @@ func isSet[T any](arr []T, index int) bool {
 // Iter iterates the items in the concurrent slice.
 // Each item is sent over a channel, so that
 // we can iterate over the slice using the builtin range keyword.
-func (cs *ThreadSafe[T]) Iter() <-chan Item[T] {
+func (cs *Safe[T]) Iter() <-chan Item[T] {
 	c := make(chan Item[T])
 	f := func() {
 		cs.RLock()
diff --git a/internal/validation/boolean.go b/internal/validation/boolean.go
index 8f16584..41dd894 100644
--- a/internal/validation/boolean.go
+++ b/internal/validation/boolean.go
@@ -24,7 +24,7 @@
 
 package validation
 
-import "github.com/pkg/errors"
+import "errors"
 
 // booleanValidator implements Validator.
 type booleanValidator struct {
diff --git a/internal/validation/pattern.go b/internal/validation/pattern.go
new file mode 100644
index 0000000..22f77d2
--- /dev/null
+++ b/internal/validation/pattern.go
@@ -0,0 +1,61 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2024 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package validation
+
+import (
+	"errors"
+	"regexp"
+)
+
+// patternValidator is used to perform a validation
+// provided a given pattern
+type patternValidator struct {
+	pattern    string
+	expression string
+	customErr  error
+}
+
+var _ Validator = (*patternValidator)(nil)
+
+// NewPatternValidator creates an instance of the validator
+// The given pattern should be valid regular expression
+func NewPatternValidator(pattern, expression string, customErr error) Validator {
+	return &patternValidator{
+		pattern:    pattern,
+		expression: expression,
+		customErr:  customErr,
+	}
+}
+
+// Validate executes the validation
+func (x *patternValidator) Validate() error {
+	if match, _ := regexp.MatchString(x.pattern, x.expression); !match {
+		if x.customErr != nil {
+			return x.customErr
+		}
+		return errors.New("invalid expression")
+	}
+	return nil
+}
diff --git a/internal/validation/tcp_address.go b/internal/validation/tcp_address.go
index a05a572..cc2c0ef 100644
--- a/internal/validation/tcp_address.go
+++ b/internal/validation/tcp_address.go
@@ -25,15 +25,14 @@
 package validation
 
 import (
+	"errors"
 	"fmt"
 	"net"
 	"strconv"
 	"strings"
-
-	"github.com/pkg/errors"
 )
 
-var errFmt = "invalid address=(%s)"
+var errFmt = "invalid address=(%s): %w"
 
 // TCPAddressValidator helps validate a TCP address
 type TCPAddressValidator struct {
@@ -52,18 +51,18 @@ func NewTCPAddressValidator(address string) *TCPAddressValidator {
 func (a *TCPAddressValidator) Validate() error {
 	host, port, err := net.SplitHostPort(strings.TrimSpace(a.address))
 	if err != nil {
-		return errors.Wrapf(err, errFmt, a.address)
+		return fmt.Errorf(errFmt, a.address, err)
 	}
 
 	// let us validate the port number
 	portNum, err := strconv.Atoi(port)
 	if err != nil {
-		return errors.Wrapf(err, errFmt, a.address)
+		return fmt.Errorf(errFmt, a.address, err)
 	}
 
 	// TODO: maybe we only need to check port number not to be negative
 	if host == "" || portNum > 65535 || portNum < 1 {
-		return fmt.Errorf(errFmt, a.address)
+		return fmt.Errorf(errFmt, a.address, errors.New("invalid address"))
 	}
 
 	return nil
diff --git a/router/broadcast.go b/router/broadcast.go
deleted file mode 100644
index 1b66ddd..0000000
--- a/router/broadcast.go
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-)
-
-// Broadcaster defines a broadcast router. This is a pool router
-// When the router receives a message to broadcast, every routee is checked whether alive or not.
-// When a routee is not alive the router removes it from its set of routeesMap.
-// When the last routee stops the router itself stops.
-type Broadcaster struct {
-	// list of routees
-	routeesMap map[string]actors.PID
-	routees    []actors.Actor
-}
-
-// enforce compilation error
-var _ actors.Actor = (*Broadcaster)(nil)
-
-// NewBroadcaster creates an instance of Broadcaster router
-// routeeRefs can be of different types as long as they can handle the router broadcast message
-func NewBroadcaster(routees ...actors.Actor) *Broadcaster {
-	// create the router instance
-	router := &Broadcaster{
-		routeesMap: make(map[string]actors.PID, len(routees)),
-		routees:    routees,
-	}
-	return router
-}
-
-// PreStart pre-starts the actor.
-func (x *Broadcaster) PreStart(context.Context) error {
-	return nil
-}
-
-// Receive handles messages sent to the Broadcaster router
-func (x *Broadcaster) Receive(ctx actors.ReceiveContext) {
-	message := ctx.Message()
-	switch message.(type) {
-	case *goaktpb.PostStart:
-		x.postStart(ctx)
-	default:
-		ctx.Unhandled()
-	}
-}
-
-// PostStop is executed when the actor is shutting down.
-func (x *Broadcaster) PostStop(context.Context) error {
-	return nil
-}
-
-// postStart spawns routeesMap
-func (x *Broadcaster) postStart(ctx actors.ReceiveContext) {
-	for index, routee := range x.routees {
-		name := fmt.Sprintf("routee-%s-%d", ctx.Self().Name(), index)
-		routee := ctx.Spawn(name, routee)
-		x.routeesMap[routee.ID()] = routee
-	}
-
-	ctx.Become(x.broadcast)
-}
-
-// broadcast send message to all the routeesMap
-func (x *Broadcaster) broadcast(ctx actors.ReceiveContext) {
-	var message *goaktpb.Broadcast
-	switch msg := ctx.Message().(type) {
-	case *goaktpb.Broadcast:
-		message = msg
-	case *goaktpb.Terminated:
-		delete(x.routeesMap, msg.GetActorId())
-		return
-	default:
-		ctx.Unhandled()
-		return
-	}
-
-	if !x.canProceed() {
-		// push message to deadletters
-		ctx.Unhandled()
-		// shutdown
-		ctx.Shutdown()
-		return
-	}
-
-	msg, err := message.GetMessage().UnmarshalNew()
-	if err != nil {
-		ctx.Err(err)
-		return
-	}
-
-	for _, routee := range x.routeesMap {
-		routee := routee
-		go func(pid actors.PID) {
-			ctx.Tell(pid, msg)
-		}(routee)
-	}
-}
-
-// canProceed check whether there are available routeesMap to proceed
-func (x *Broadcaster) canProceed() bool {
-	for _, routee := range x.routeesMap {
-		if !routee.IsRunning() {
-			delete(x.routeesMap, routee.ID())
-		}
-	}
-	return len(x.routeesMap) > 0
-}
diff --git a/router/random.go b/router/random.go
deleted file mode 100644
index b87cabd..0000000
--- a/router/random.go
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-	"math/rand/v2"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-)
-
-// Random defines a Random router.
-// The router selects a routee at random when a message is sent through the router.
-type Random struct {
-	// list of routees
-	routeesMap map[string]actors.PID
-	routees    []actors.Actor
-}
-
-// enforce compilation error
-var _ actors.Actor = (*Random)(nil)
-
-// NewRandom creates an instance of Random router
-func NewRandom(routees ...actors.Actor) *Random {
-	// create the router instance
-	router := &Random{
-		routeesMap: make(map[string]actors.PID, len(routees)),
-		routees:    routees,
-	}
-	return router
-}
-
-// PreStart pre-starts the actor.
-func (x *Random) PreStart(context.Context) error {
-	return nil
-}
-
-// Receive handles messages sent to the Random router
-func (x *Random) Receive(ctx actors.ReceiveContext) {
-	message := ctx.Message()
-	switch message.(type) {
-	case *goaktpb.PostStart:
-		x.postStart(ctx)
-	default:
-		ctx.Unhandled()
-	}
-}
-
-// PostStop is executed when the actor is shutting down.
-func (x *Random) PostStop(context.Context) error {
-	return nil
-}
-
-// postStart spawns routeeRefs
-func (x *Random) postStart(ctx actors.ReceiveContext) {
-	for index, routee := range x.routees {
-		name := fmt.Sprintf("routee-%s-%d", ctx.Self().Name(), index)
-		routee := ctx.Spawn(name, routee)
-		x.routeesMap[routee.ID()] = routee
-	}
-	ctx.Become(x.broadcast)
-}
-
-// broadcast send message to all the routeeRefs
-func (x *Random) broadcast(ctx actors.ReceiveContext) {
-	var message *goaktpb.Broadcast
-	switch msg := ctx.Message().(type) {
-	case *goaktpb.Broadcast:
-		message = msg
-	case *goaktpb.Terminated:
-		delete(x.routeesMap, msg.GetActorId())
-		return
-	default:
-		ctx.Unhandled()
-		return
-	}
-
-	routees := make([]actors.PID, 0, len(x.routeesMap))
-	for _, routee := range x.routeesMap {
-		if routee.IsRunning() {
-			routees = append(routees, routee)
-		}
-	}
-
-	if len(routees) == 0 {
-		// push message to deadletters
-		ctx.Unhandled()
-		return
-	}
-
-	msg, err := message.GetMessage().UnmarshalNew()
-	if err != nil {
-		ctx.Err(err)
-	}
-
-	routee := routees[rand.IntN(len(routees))]
-	ctx.Tell(routee, msg)
-}
diff --git a/router/round_robin.go b/router/round_robin.go
deleted file mode 100644
index dfc170c..0000000
--- a/router/round_robin.go
+++ /dev/null
@@ -1,123 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-	"sync/atomic"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-)
-
-// RoundRobin defines a Random router.
-// It rotates over the set of routeesMap making sure that if there are n routeesMap,
-// then for n messages sent through the router, each actor is forwarded one message.
-type RoundRobin struct {
-	// list of routees
-	routeesMap map[string]actors.PID
-	routees    []actors.Actor
-	next       uint32
-}
-
-// enforce compilation error
-var _ actors.Actor = (*RoundRobin)(nil)
-
-// NewRoundRobin creates an instance of RoundRobin router
-func NewRoundRobin(routees ...actors.Actor) *RoundRobin {
-	// create the router instance
-	router := &RoundRobin{
-		routeesMap: make(map[string]actors.PID, len(routees)),
-		routees:    routees,
-	}
-	return router
-}
-
-// PreStart pre-starts the actor.
-func (x *RoundRobin) PreStart(context.Context) error {
-	return nil
-}
-
-// Receive handles messages sent to the Random router
-func (x *RoundRobin) Receive(ctx actors.ReceiveContext) {
-	message := ctx.Message()
-	switch message.(type) {
-	case *goaktpb.PostStart:
-		x.postStart(ctx)
-	default:
-		ctx.Unhandled()
-	}
-}
-
-// PostStop is executed when the actor is shutting down.
-func (x *RoundRobin) PostStop(context.Context) error {
-	return nil
-}
-
-// postStart spawns routees
-func (x *RoundRobin) postStart(ctx actors.ReceiveContext) {
-	for index, routee := range x.routees {
-		name := fmt.Sprintf("routee-%s-%d", ctx.Self().Name(), index)
-		routee := ctx.Spawn(name, routee)
-		x.routeesMap[routee.ID()] = routee
-	}
-	ctx.Become(x.broadcast)
-}
-
-// broadcast send message to all the routees
-func (x *RoundRobin) broadcast(ctx actors.ReceiveContext) {
-	var message *goaktpb.Broadcast
-	switch msg := ctx.Message().(type) {
-	case *goaktpb.Broadcast:
-		message = msg
-	case *goaktpb.Terminated:
-		delete(x.routeesMap, msg.GetActorId())
-		return
-	default:
-		ctx.Unhandled()
-		return
-	}
-
-	routees := make([]actors.PID, 0, len(x.routeesMap))
-	for _, routee := range x.routeesMap {
-		if routee.IsRunning() {
-			routees = append(routees, routee)
-		}
-	}
-
-	if len(routees) == 0 {
-		// push message to deadletters
-		ctx.Unhandled()
-	}
-
-	msg, err := message.GetMessage().UnmarshalNew()
-	if err != nil {
-		ctx.Err(err)
-	}
-	n := atomic.AddUint32(&x.next, 1)
-	routee := routees[(int(n)-1)%len(routees)]
-	ctx.Tell(routee, msg)
-}


Test patch:
diff --git a/actors/actor_system_test.go b/actors/actor_system_test.go
index 8339bb7..036d30b 100644
--- a/actors/actor_system_test.go
+++ b/actors/actor_system_test.go
@@ -89,7 +89,7 @@ func TestActorSystem(t *testing.T) {
 	})
 	t.Run("With Spawn an actor when started", func(t *testing.T) {
 		ctx := context.TODO()
-		sys, _ := NewActorSystem("testSys", WithLogger(log.DiscardLogger), WithMailbox(newReceiveContextBuffer(100)))
+		sys, _ := NewActorSystem("testSys", WithLogger(log.DefaultLogger), WithMailbox(newReceiveContextBuffer(100)))
 
 		// start the actor system
 		err := sys.Start(ctx)
@@ -840,8 +840,9 @@ func TestActorSystem(t *testing.T) {
 		time.Sleep(time.Second)
 
 		t.Cleanup(func() {
-			err = sys.Stop(ctx)
-			assert.Error(t, err)
+			assert.Panics(t, func() {
+				_ = sys.Stop(ctx)
+			})
 		})
 	})
 	t.Run("With deadletters subscription ", func(t *testing.T) {
@@ -862,7 +863,7 @@ func TestActorSystem(t *testing.T) {
 
 		// create the black hole actor
 		actor := &discarder{}
-		actorRef, err := sys.Spawn(ctx, "discarder ", actor)
+		actorRef, err := sys.Spawn(ctx, "discarder", actor)
 		assert.NoError(t, err)
 		assert.NotNil(t, actorRef)
 
@@ -1240,7 +1241,7 @@ func TestActorSystem(t *testing.T) {
 	})
 	t.Run("With SpawnFromFunc with PreStop error", func(t *testing.T) {
 		ctx := context.TODO()
-		sys, _ := NewActorSystem("testSys", WithLogger(log.DiscardLogger))
+		sys, _ := NewActorSystem("testSys", WithLogger(log.DefaultLogger))
 
 		// start the actor system
 		err := sys.Start(ctx)
@@ -1267,8 +1268,9 @@ func TestActorSystem(t *testing.T) {
 		require.NoError(t, Tell(ctx, actorRef, &testpb.Reply{Content: "test spawn from func"}))
 
 		t.Cleanup(func() {
-			err = sys.Stop(ctx)
-			assert.Error(t, err)
+			assert.Panics(t, func() {
+				_ = sys.Stop(ctx)
+			})
 		})
 	})
 	t.Run("With SpawnFromFunc with actorSystem not started", func(t *testing.T) {
diff --git a/actors/actor_test.go b/actors/actor_test.go
index c500e69..c4f07fc 100644
--- a/actors/actor_test.go
+++ b/actors/actor_test.go
@@ -43,6 +43,7 @@ import (
 	"github.com/tochemey/goakt/v2/discovery/nats"
 	"github.com/tochemey/goakt/v2/goaktpb"
 	"github.com/tochemey/goakt/v2/log"
+	"github.com/tochemey/goakt/v2/test/data/testpb"
 	testspb "github.com/tochemey/goakt/v2/test/data/testpb"
 )
 
@@ -107,23 +108,23 @@ func (p *testActor) Receive(ctx ReceiveContext) {
 	}
 }
 
-// supervisor is an actor that monitors another actor
+// testSupervisor is an actor that monitors another actor
 // and reacts to its failure.
-type supervisor struct{}
+type testSupervisor struct{}
 
 // enforce compilation error
-var _ Actor = (*supervisor)(nil)
+var _ Actor = (*testSupervisor)(nil)
 
-// newSupervisor creates an instance of supervisor
-func newSupervisor() *supervisor {
-	return &supervisor{}
+// newTestSupervisor creates an instance of testSupervisor
+func newTestSupervisor() *testSupervisor {
+	return &testSupervisor{}
 }
 
-func (p *supervisor) PreStart(context.Context) error {
+func (p *testSupervisor) PreStart(context.Context) error {
 	return nil
 }
 
-func (p *supervisor) Receive(ctx ReceiveContext) {
+func (p *testSupervisor) Receive(ctx ReceiveContext) {
 	switch ctx.Message().(type) {
 	case *goaktpb.PostStart:
 	case *testspb.TestSend:
@@ -132,26 +133,26 @@ func (p *supervisor) Receive(ctx ReceiveContext) {
 	}
 }
 
-func (p *supervisor) PostStop(context.Context) error {
+func (p *testSupervisor) PostStop(context.Context) error {
 	return nil
 }
 
-// supervised is an actor that is monitored
-type supervised struct{}
+// testSupervised is an actor that is monitored
+type testSupervised struct{}
 
 // enforce compilation error
-var _ Actor = (*supervised)(nil)
+var _ Actor = (*testSupervised)(nil)
 
-// newSupervised creates an instance of supervised
-func newSupervised() *supervised {
-	return &supervised{}
+// newTestSupervised creates an instance of testSupervised
+func newTestSupervised() *testSupervised {
+	return &testSupervised{}
 }
 
-func (x *supervised) PreStart(context.Context) error {
+func (x *testSupervised) PreStart(context.Context) error {
 	return nil
 }
 
-func (x *supervised) Receive(ctx ReceiveContext) {
+func (x *testSupervised) Receive(ctx ReceiveContext) {
 	switch ctx.Message().(type) {
 	case *goaktpb.PostStart:
 	case *testspb.TestSend:
@@ -164,7 +165,7 @@ func (x *supervised) Receive(ctx ReceiveContext) {
 	}
 }
 
-func (x *supervised) PostStop(context.Context) error {
+func (x *testSupervised) PostStop(context.Context) error {
 	return nil
 }
 
@@ -494,3 +495,36 @@ func startClusterSystem(t *testing.T, nodeName, serverAddr string) (ActorSystem,
 type unhandledSupervisorDirective struct{}
 
 func (x unhandledSupervisorDirective) isSupervisorDirective() {}
+
+type worker struct {
+	counter int
+	logger  log.Logger
+}
+
+var _ Actor = (*worker)(nil)
+
+func newWorker() *worker {
+	return &worker{}
+}
+
+func (x *worker) PreStart(context.Context) error {
+	x.logger = log.DefaultLogger
+	return nil
+}
+
+func (x *worker) Receive(ctx ReceiveContext) {
+	switch msg := ctx.Message().(type) {
+	case *testpb.DoLog:
+		x.counter++
+		x.logger.Infof("Got message: %s", msg.GetText())
+	case *testpb.GetCount:
+		x.counter++
+		ctx.Response(&testpb.Count{Value: int32(x.counter)})
+	default:
+		ctx.Unhandled()
+	}
+}
+
+func (x *worker) PostStop(context.Context) error {
+	return nil
+}
diff --git a/actors/context_test.go b/actors/context_test.go
index fb5e8ea..406feea 100644
--- a/actors/context_test.go
+++ b/actors/context_test.go
@@ -685,7 +685,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -705,7 +705,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -723,7 +723,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -746,7 +746,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		assert.Panics(t, func() {
-			context.Spawn("SpawnChild", newSupervised())
+			context.Spawn("SpawnChild", newTestSupervised())
 		})
 	})
 	t.Run("With not found Child", func(t *testing.T) {
@@ -755,7 +755,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -775,7 +775,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -796,7 +796,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -816,7 +816,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -837,7 +837,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -857,7 +857,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -878,7 +878,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -898,7 +898,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -921,7 +921,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -955,7 +955,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -974,7 +974,7 @@ func TestReceiveContext(t *testing.T) {
 		}
 
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
@@ -995,7 +995,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1016,7 +1016,7 @@ func TestReceiveContext(t *testing.T) {
 		// create the child actor
 		childPath := NewPath("child", NewAddress("sys", "host", 1))
 		child, err := newPID(ctx, childPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1039,7 +1039,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1059,7 +1059,7 @@ func TestReceiveContext(t *testing.T) {
 
 		// create the child actor
 		name := "monitored"
-		child := context.Spawn(name, newSupervised())
+		child := context.Spawn(name, newTestSupervised())
 		assert.NotNil(t, child)
 		assert.Len(t, context.Children(), 1)
 
diff --git a/actors/pid_test.go b/actors/pid_test.go
index 2439707..6644c60 100644
--- a/actors/pid_test.go
+++ b/actors/pid_test.go
@@ -388,7 +388,7 @@ func TestSupervisorStrategy(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withSupervisorDirective(NewStopDirective()),
@@ -398,11 +398,12 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
 		assert.Len(t, parent.Children(), 1)
+		assert.Len(t, child.Parents(), 1)
 		// let us send 10 public to the actors
 		count := 10
 		for i := 0; i < count; i++ {
@@ -423,7 +424,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withPassivationDisabled(),
@@ -433,7 +434,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -463,7 +464,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withPassivationDisabled(),
@@ -504,7 +505,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withSupervisorDirective(DefaultSupervisoryStrategy),
@@ -546,7 +547,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(logger),
 			withPassivationDisabled(),
@@ -557,7 +558,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -585,7 +586,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withPassivationDisabled(),
@@ -595,7 +596,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -624,7 +625,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(logger),
 			withPassivationDisabled(),
@@ -635,7 +636,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -675,7 +676,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		// create the parent actor
 		parent, err := newPID(ctx,
 			actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(logger),
 			withPassivationDisabled(),
@@ -686,7 +687,7 @@ func TestSupervisorStrategy(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -994,7 +995,7 @@ func TestSpawnChild(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1003,7 +1004,7 @@ func TestSpawnChild(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -1014,7 +1015,7 @@ func TestSpawnChild(t *testing.T) {
 
 		time.Sleep(100 * time.Millisecond)
 		// create the child actor
-		child, err = parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err = parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -1033,7 +1034,7 @@ func TestSpawnChild(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1042,7 +1043,7 @@ func TestSpawnChild(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -1050,7 +1051,7 @@ func TestSpawnChild(t *testing.T) {
 
 		time.Sleep(100 * time.Millisecond)
 		// create the child actor
-		child, err = parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err = parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -1069,7 +1070,7 @@ func TestSpawnChild(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1083,7 +1084,7 @@ func TestSpawnChild(t *testing.T) {
 		assert.NoError(t, err)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.Error(t, err)
 		assert.EqualError(t, err, ErrDead.Error())
 		assert.Nil(t, child)
@@ -1096,7 +1097,7 @@ func TestSpawnChild(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
@@ -1128,7 +1129,7 @@ func TestSpawnChild(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withEventsStream(eventsStream),
@@ -1138,7 +1139,7 @@ func TestSpawnChild(t *testing.T) {
 		assert.NotNil(t, parent)
 
 		// create the child actor
-		child, err := parent.SpawnChild(ctx, "SpawnChild", newSupervised())
+		child, err := parent.SpawnChild(ctx, "SpawnChild", newTestSupervised())
 		assert.NoError(t, err)
 		assert.NotNil(t, child)
 
@@ -1332,7 +1333,7 @@ func TestShutdown(t *testing.T) {
 
 		// create the parent actor
 		parent, err := newPID(ctx, actorPath,
-			newSupervisor(),
+			newTestSupervisor(),
 			withInitMaxRetries(1),
 			withCustomLogger(log.DiscardLogger),
 			withAskTimeout(replyTimeout))
diff --git a/actors/router_test.go b/actors/router_test.go
new file mode 100644
index 0000000..46ec402
--- /dev/null
+++ b/actors/router_test.go
@@ -0,0 +1,315 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2024 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package actors
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/types/known/anypb"
+
+	"github.com/tochemey/goakt/v2/goaktpb"
+	"github.com/tochemey/goakt/v2/log"
+	"github.com/tochemey/goakt/v2/test/data/testpb"
+)
+
+func TestRouter(t *testing.T) {
+	t.Run("With Fan-Out strategy", func(t *testing.T) {
+		ctx := context.TODO()
+		logger := log.DefaultLogger
+		system, err := NewActorSystem(
+			"testSystem",
+			WithPassivationDisabled(),
+			WithLogger(logger),
+			WithReplyTimeout(time.Minute))
+
+		require.NoError(t, err)
+		require.NotNil(t, system)
+
+		require.NoError(t, system.Start(ctx))
+
+		time.Sleep(time.Second)
+
+		routeesKind := new(worker)
+		poolSize := 2
+		routingStrategy := FanOutRouting
+		router, err := system.SpawnRouter(ctx, poolSize, routeesKind, WithRoutingStrategy(routingStrategy))
+		require.NoError(t, err)
+		require.NotNil(t, router)
+
+		time.Sleep(time.Second)
+
+		// send a broadcast message to the router
+		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
+		err = Tell(ctx, router, &goaktpb.Broadcast{Message: message})
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+
+		// this is just for tests purpose
+		workerOneName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 0)
+		workerTwoName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 1)
+
+		workerOneRef, err := system.LocalActor(workerOneName)
+		require.NoError(t, err)
+		require.NotNil(t, workerOneRef)
+
+		workerTwoRef, err := system.LocalActor(workerTwoName)
+		require.NoError(t, err)
+		require.NotNil(t, workerTwoRef)
+
+		expected := &testpb.Count{Value: 2}
+
+		reply, err := Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
+		require.NoError(t, err)
+		require.NotNil(t, reply)
+		assert.True(t, proto.Equal(expected, reply))
+
+		reply, err = Ask(ctx, workerTwoRef, new(testpb.GetCount), time.Minute)
+		require.NoError(t, err)
+		require.NotNil(t, reply)
+		assert.True(t, proto.Equal(expected, reply))
+
+		t.Cleanup(func() {
+			assert.NoError(t, system.Stop(ctx))
+		})
+	})
+	t.Run("With Fan-Out strategy With no available routees router shuts down", func(t *testing.T) {
+		ctx := context.TODO()
+		logger := log.DefaultLogger
+		system, err := NewActorSystem(
+			"testSystem",
+			WithPassivationDisabled(),
+			WithLogger(logger),
+			WithReplyTimeout(time.Minute))
+
+		require.NoError(t, err)
+		require.NotNil(t, system)
+
+		require.NoError(t, system.Start(ctx))
+
+		time.Sleep(time.Second)
+
+		routeesKind := new(worker)
+		poolSize := 2
+		routingStrategy := FanOutRouting
+		router, err := system.SpawnRouter(ctx, poolSize, routeesKind, WithRoutingStrategy(routingStrategy))
+		require.NoError(t, err)
+		require.NotNil(t, router)
+
+		time.Sleep(time.Second)
+
+		// this is just for tests purpose
+		workerOneName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 0)
+		workerTwoName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 1)
+
+		require.NoError(t, system.Kill(ctx, workerOneName))
+		require.NoError(t, system.Kill(ctx, workerTwoName))
+
+		// send a broadcast message to the router
+		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
+		err = Tell(ctx, router, &goaktpb.Broadcast{Message: message})
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+
+		ref, err := system.LocalActor("worker-pool")
+		require.Error(t, err)
+		require.Nil(t, ref)
+		assert.EqualError(t, err, ErrActorNotFound("worker-pool").Error())
+
+		t.Cleanup(func() {
+			assert.NoError(t, system.Stop(ctx))
+		})
+	})
+	t.Run("With Round Robin strategy", func(t *testing.T) {
+		ctx := context.TODO()
+		logger := log.DefaultLogger
+		system, err := NewActorSystem(
+			"testSystem",
+			WithPassivationDisabled(),
+			WithLogger(logger),
+			WithReplyTimeout(time.Minute))
+
+		require.NoError(t, err)
+		require.NotNil(t, system)
+
+		require.NoError(t, system.Start(ctx))
+
+		time.Sleep(time.Second)
+
+		routeesKind := new(worker)
+		poolSize := 1
+		routingStrategy := RoundRobinRouting
+		router, err := system.SpawnRouter(ctx, poolSize, routeesKind, WithRoutingStrategy(routingStrategy))
+		require.NoError(t, err)
+		require.NotNil(t, router)
+
+		time.Sleep(time.Second)
+
+		// send a broadcast message to the router
+		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
+		err = Tell(ctx, router, &goaktpb.Broadcast{Message: message})
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+
+		// this is just for tests purpose
+		workerName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 0)
+
+		workerOneRef, err := system.LocalActor(workerName)
+		require.NoError(t, err)
+		require.NotNil(t, workerOneRef)
+
+		expected := &testpb.Count{Value: 2}
+
+		reply, err := Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
+		require.NoError(t, err)
+		require.NotNil(t, reply)
+		assert.True(t, proto.Equal(expected, reply))
+
+		t.Cleanup(func() {
+			assert.NoError(t, system.Stop(ctx))
+		})
+	})
+	t.Run("With Round Robin strategy With no available routees router is alive and message in deadletter", func(t *testing.T) {
+		ctx := context.TODO()
+		logger := log.DefaultLogger
+
+		system, err := NewActorSystem(
+			"testSystem",
+			WithPassivationDisabled(),
+			WithLogger(logger),
+			WithReplyTimeout(time.Minute))
+
+		require.NoError(t, err)
+		require.NotNil(t, system)
+
+		require.NoError(t, system.Start(ctx))
+
+		time.Sleep(time.Second)
+
+		// create a deadletter subscriber
+		consumer, err := system.Subscribe()
+		require.NoError(t, err)
+		require.NotNil(t, consumer)
+
+		routeesKind := new(worker)
+		poolSize := 1
+		routingStrategy := RoundRobinRouting
+		router, err := system.SpawnRouter(ctx, poolSize, routeesKind, WithRoutingStrategy(routingStrategy))
+
+		require.NoError(t, err)
+		require.NotNil(t, router)
+
+		time.Sleep(time.Second)
+
+		// this is just for tests purpose
+		workerName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 0)
+		err = system.Kill(ctx, workerName)
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+
+		// send a broadcast message to the router
+		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
+		err = Tell(ctx, router, &goaktpb.Broadcast{Message: message})
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+		require.False(t, router.IsRunning())
+
+		var items []*goaktpb.Deadletter
+		for message := range consumer.Iterator() {
+			payload := message.Payload()
+			// only listening to deadletters
+			deadletter, ok := payload.(*goaktpb.Deadletter)
+			if ok {
+				items = append(items, deadletter)
+			}
+		}
+
+		require.NotEmpty(t, items)
+
+		t.Cleanup(func() {
+			assert.NoError(t, system.Stop(ctx))
+		})
+	})
+	t.Run("With Random strategy", func(t *testing.T) {
+		ctx := context.TODO()
+		logger := log.DefaultLogger
+		system, err := NewActorSystem(
+			"testSystem",
+			WithPassivationDisabled(),
+			WithLogger(logger),
+			WithReplyTimeout(time.Minute))
+
+		require.NoError(t, err)
+		require.NotNil(t, system)
+
+		require.NoError(t, system.Start(ctx))
+
+		time.Sleep(time.Second)
+
+		routeesKind := new(worker)
+		poolSize := 1
+		routingStrategy := RandomRouting
+		router, err := system.SpawnRouter(ctx, poolSize, routeesKind, WithRoutingStrategy(routingStrategy))
+		require.NoError(t, err)
+		require.NotNil(t, router)
+
+		time.Sleep(time.Second)
+
+		// send a broadcast message to the router
+		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
+		err = Tell(ctx, router, &goaktpb.Broadcast{Message: message})
+		require.NoError(t, err)
+
+		time.Sleep(time.Second)
+
+		// this is just for tests purpose
+		workerName := fmt.Sprintf("GoAktRoutee-%s-%d", router.Name(), 0)
+
+		workerOneRef, err := system.LocalActor(workerName)
+		require.NoError(t, err)
+		require.NotNil(t, workerOneRef)
+
+		expected := &testpb.Count{Value: 2}
+
+		reply, err := Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
+		require.NoError(t, err)
+		require.NotNil(t, reply)
+		assert.True(t, proto.Equal(expected, reply))
+
+		t.Cleanup(func() {
+			assert.NoError(t, system.Stop(ctx))
+		})
+	})
+}
diff --git a/router/router_test.go b/actors/system_supervisor.go
similarity index 60%
rename from router/router_test.go
rename to actors/system_supervisor.go
index 0297c67..bf7b768 100644
--- a/router/router_test.go
+++ b/actors/system_supervisor.go
@@ -22,45 +22,46 @@
  * SOFTWARE.
  */
 
-package router
+package actors
 
 import (
 	"context"
 
-	"github.com/tochemey/goakt/v2/actors"
+	"github.com/tochemey/goakt/v2/goaktpb"
 	"github.com/tochemey/goakt/v2/log"
-	"github.com/tochemey/goakt/v2/test/data/testpb"
 )
 
-type worker struct {
-	counter int
-	logger  log.Logger
+// systemSupervisor is an actor which roles is to handle
+// escalation failure
+type systemSupervisor struct {
+	logger log.Logger
 }
 
-var _ actors.Actor = (*worker)(nil)
+// enforce compilation error
+var _ Actor = (*systemSupervisor)(nil)
 
-func newWorker() *worker {
-	return &worker{}
+// newSupervisor creates an instance of a testSupervisor
+func newSystemSupervisor(logger log.Logger) *systemSupervisor {
+	return &systemSupervisor{
+		logger: logger,
+	}
 }
 
-func (x *worker) PreStart(context.Context) error {
-	x.logger = log.DefaultLogger
+func (s *systemSupervisor) PreStart(context.Context) error {
+	s.logger.Info("starting the system supervisor actor")
 	return nil
 }
 
-func (x *worker) Receive(ctx actors.ReceiveContext) {
-	switch msg := ctx.Message().(type) {
-	case *testpb.DoLog:
-		x.counter++
-		x.logger.Infof("Got message: %s", msg.GetText())
-	case *testpb.GetCount:
-		x.counter++
-		ctx.Response(&testpb.Count{Value: int32(x.counter)})
+func (s *systemSupervisor) Receive(ctx ReceiveContext) {
+	switch ctx.Message().(type) {
+	case *goaktpb.PostStart:
+		s.logger.Info("system supervior successfully started")
 	default:
 		ctx.Unhandled()
 	}
 }
 
-func (x *worker) PostStop(context.Context) error {
+func (s *systemSupervisor) PostStop(context.Context) error {
+	s.logger.Info("system supervior stopped")
 	return nil
 }
diff --git a/client/client_test.go b/client/client_test.go
index 5610a8e..a785335 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -131,7 +131,7 @@ func TestClient(t *testing.T) {
 			time.Sleep(time.Second)
 		})
 	})
-	t.Run("With Random strategy", func(t *testing.T) {
+	t.Run("With randomRouter strategy", func(t *testing.T) {
 		ctx := context.TODO()
 		logger := log.DiscardLogger
 		// start the NATS server
diff --git a/internal/slices/slices_test.go b/internal/slices/slices_test.go
index c023669..9f547b4 100644
--- a/internal/slices/slices_test.go
+++ b/internal/slices/slices_test.go
@@ -32,7 +32,7 @@ import (
 
 func TestNewThreadSafeSlice(t *testing.T) {
 	// create a concurrent slice of integer
-	sl := NewThreadSafe[int]()
+	sl := NewSafe[int]()
 
 	// add some items
 	sl.Append(2)
diff --git a/internal/validation/pattern_test.go b/internal/validation/pattern_test.go
new file mode 100644
index 0000000..b0d3e2e
--- /dev/null
+++ b/internal/validation/pattern_test.go
@@ -0,0 +1,47 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2022-2024 Tochemey
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package validation
+
+import (
+	"errors"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestPatternValidator(t *testing.T) {
+	pattern := "^[a-zA-Z0-9][a-zA-Z0-9-_\\.]*$"
+	expression := "discarder"
+	validator := NewPatternValidator(pattern, expression, nil)
+	assert.NoError(t, validator.Validate())
+
+	expression = "$omeN@me"
+	customError := errors.New("custom error")
+	validator = NewPatternValidator(pattern, expression, customError)
+	err := validator.Validate()
+	require.Error(t, err)
+	assert.EqualError(t, err, customError.Error())
+}
diff --git a/router/broadcast_test.go b/router/broadcast_test.go
deleted file mode 100644
index c5c8c88..0000000
--- a/router/broadcast_test.go
+++ /dev/null
@@ -1,153 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/types/known/anypb"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-	"github.com/tochemey/goakt/v2/log"
-	"github.com/tochemey/goakt/v2/test/data/testpb"
-)
-
-func TestBroadcast(t *testing.T) {
-	t.Run("With happy path", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a broadcast router with two routeeRefs
-		broadcaster := NewBroadcaster(newWorker(), newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", broadcaster)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerOneName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-		workerTwoName := fmt.Sprintf("routee-%s-%d", router.Name(), 1)
-
-		workerOneRef, err := system.LocalActor(workerOneName)
-		require.NoError(t, err)
-		require.NotNil(t, workerOneRef)
-
-		workerTwoRef, err := system.LocalActor(workerTwoName)
-		require.NoError(t, err)
-		require.NotNil(t, workerTwoRef)
-
-		expected := &testpb.Count{Value: 2}
-
-		reply, err := actors.Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
-		require.NoError(t, err)
-		require.NotNil(t, reply)
-		assert.True(t, proto.Equal(expected, reply))
-
-		reply, err = actors.Ask(ctx, workerTwoRef, new(testpb.GetCount), time.Minute)
-		require.NoError(t, err)
-		require.NotNil(t, reply)
-		assert.True(t, proto.Equal(expected, reply))
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-	t.Run("With no available routees router shuts down", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a broadcast router with two routeeRefs
-		broadcaster := NewBroadcaster(newWorker(), newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", broadcaster)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerOneName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-		workerTwoName := fmt.Sprintf("routee-%s-%d", router.Name(), 1)
-
-		require.NoError(t, system.Kill(ctx, workerOneName))
-		require.NoError(t, system.Kill(ctx, workerTwoName))
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-
-		ref, err := system.LocalActor("worker-pool")
-		require.Error(t, err)
-		require.Nil(t, ref)
-		assert.EqualError(t, err, actors.ErrActorNotFound("worker-pool").Error())
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-}
diff --git a/router/random_test.go b/router/random_test.go
deleted file mode 100644
index da45fab..0000000
--- a/router/random_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/types/known/anypb"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-	"github.com/tochemey/goakt/v2/log"
-	"github.com/tochemey/goakt/v2/test/data/testpb"
-)
-
-func TestRandom(t *testing.T) {
-	t.Run("With happy path", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a random router with one routee
-		// this is for the purpose of testing to make sure given routee does receive the
-		// message sent
-		random := NewRandom(newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", random)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-
-		workerOneRef, err := system.LocalActor(workerName)
-		require.NoError(t, err)
-		require.NotNil(t, workerOneRef)
-
-		expected := &testpb.Count{Value: 2}
-
-		reply, err := actors.Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
-		require.NoError(t, err)
-		require.NotNil(t, reply)
-		assert.True(t, proto.Equal(expected, reply))
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-	t.Run("With no available routees router is alive and message in deadletter", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a deadletter subscriber
-		consumer, err := system.Subscribe()
-		require.NoError(t, err)
-		require.NotNil(t, consumer)
-
-		// create a random router with one routee
-		// this is for the purpose of testing to make sure given routee does receive the
-		// message sent
-		random := NewRandom(newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", random)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-		err = system.Kill(ctx, workerName)
-		require.NoError(t, err)
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-		assert.True(t, router.IsRunning())
-
-		var items []*goaktpb.Deadletter
-		for message := range consumer.Iterator() {
-			payload := message.Payload()
-			// only listening to deadletters
-			deadletter, ok := payload.(*goaktpb.Deadletter)
-			if ok {
-				items = append(items, deadletter)
-			}
-		}
-
-		require.Len(t, items, 1)
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-}
diff --git a/router/round_robin_test.go b/router/round_robin_test.go
deleted file mode 100644
index a13ae4f..0000000
--- a/router/round_robin_test.go
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * MIT License
- *
- * Copyright (c) 2022-2024 Tochemey
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- * SOFTWARE.
- */
-
-package router
-
-import (
-	"context"
-	"fmt"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	"google.golang.org/protobuf/proto"
-	"google.golang.org/protobuf/types/known/anypb"
-
-	"github.com/tochemey/goakt/v2/actors"
-	"github.com/tochemey/goakt/v2/goaktpb"
-	"github.com/tochemey/goakt/v2/log"
-	"github.com/tochemey/goakt/v2/test/data/testpb"
-)
-
-func TestRoundRobin(t *testing.T) {
-	t.Run("With happy path", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a roundrobin router with one routee
-		// this is for the purpose of testing to make sure given routee does receive the
-		// message sent
-		roundrobin := NewRoundRobin(newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", roundrobin)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-
-		workerOneRef, err := system.LocalActor(workerName)
-		require.NoError(t, err)
-		require.NotNil(t, workerOneRef)
-
-		expected := &testpb.Count{Value: 2}
-
-		reply, err := actors.Ask(ctx, workerOneRef, new(testpb.GetCount), time.Minute)
-		require.NoError(t, err)
-		require.NotNil(t, reply)
-		assert.True(t, proto.Equal(expected, reply))
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-	t.Run("With no available routees router is alive and message in deadletter", func(t *testing.T) {
-		ctx := context.TODO()
-		logger := log.DefaultLogger
-		system, err := actors.NewActorSystem(
-			"testSystem",
-			actors.WithPassivationDisabled(),
-			actors.WithLogger(logger),
-			actors.WithReplyTimeout(time.Minute))
-
-		require.NoError(t, err)
-		require.NotNil(t, system)
-		require.NoError(t, system.Start(ctx))
-
-		time.Sleep(time.Second)
-
-		// create a deadletter subscriber
-		consumer, err := system.Subscribe()
-		require.NoError(t, err)
-		require.NotNil(t, consumer)
-
-		// create a roundRobin router with one routee
-		// this is for the purpose of testing to make sure given routee does receive the
-		// message sent
-		roundRobin := NewRoundRobin(newWorker())
-
-		router, err := system.Spawn(ctx, "worker-pool", roundRobin)
-		require.NoError(t, err)
-		require.NotNil(t, router)
-
-		time.Sleep(time.Second)
-
-		// this is just for tests purpose
-		workerName := fmt.Sprintf("routee-%s-%d", router.Name(), 0)
-		err = system.Kill(ctx, workerName)
-		require.NoError(t, err)
-
-		// send a broadcast message to the router
-		message, _ := anypb.New(&testpb.DoLog{Text: "msg"})
-		err = actors.Tell(ctx, router, &goaktpb.Broadcast{Message: message})
-		require.NoError(t, err)
-
-		time.Sleep(time.Second)
-		assert.True(t, router.IsRunning())
-
-		var items []*goaktpb.Deadletter
-		for message := range consumer.Iterator() {
-			payload := message.Payload()
-			// only listening to deadletters
-			deadletter, ok := payload.(*goaktpb.Deadletter)
-			if ok {
-				items = append(items, deadletter)
-			}
-		}
-
-		require.Len(t, items, 1)
-
-		t.Cleanup(func() {
-			assert.NoError(t, system.Stop(ctx))
-		})
-	})
-}