Repository: looplab/eventhorizon
Pull request #316 (2021-05-03 12:36:56)

Feature / Separate namespace support
### Description

**BEWARE: After this PR repos/event stores are created with their FULL DB NAME not prefix only, as it was before. To keep the old default DB name set it to `prefix + "_default"` when creating repos/event stores.**

Moves all namespace support to a separate package `namespace`. The package contains an event store and repo with namespace support that uses any underlying event store and repo as storage. The factory function provided in the `New...()` methods is used to create event stores/repos the first time a namespace is used.

```go
eventStore := NewEventStore(func(ns string) (eh.EventStore, error) {
	s, err := mongodb.NewEventStore("mongodb://", ns)
	if err != nil {
		return nil, err
	}
	return s, nil
})
```

### Affected Components

- Event Store
- Repo
- Context
- Errors with previous namespace support

### Related Issues

### Solution and Design

### Steps to test and verify


coveralls: 
[![Coverage Status](https://coveralls.io/builds/39305484/badge)](https://coveralls.io/builds/39305484)

Coverage decreased (-1.9%) to 67.919% when pulling **a70b24e6ac60b4760df9bdfce68bdac759c0f7c0 on maxekman:feature/separate-namespace-support** into **325e71820f8c4033fd822124aaa07eac074bac1c on looplab:main**.


Patch:
diff --git a/README.md b/README.md
index c6b3b15..d843d7d 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ See the example folder for a few examples to get you started.
 - Cache - Adds support for in-memory caching of entities from an underlying repo.
 - Tracing - Adds distributed tracing support to an repo operations with OpenTracing.
 
-## Development
+# Development
 
 To develop Event Horizon you need to have Docker and Docker Compose installed.
 
diff --git a/context.go b/context.go
index e666a9c..e1bd3d4 100644
--- a/context.go
+++ b/context.go
@@ -22,16 +22,12 @@ import (
 	"github.com/looplab/eventhorizon/uuid"
 )
 
-// DefaultNamespace is the namespace to use if not set in the context.
-const DefaultNamespace = "default"
-
 // DefaultMinVersionDeadline is the deadline to use when creating a min version
 // context that waits.
 const DefaultMinVersionDeadline = 10 * time.Second
 
 // Strings used to marshal context values.
 const (
-	namespaceKeyStr     = "eh_namespace"
 	aggregateIDKeyStr   = "eh_aggregate_id"
 	aggregateTypeKeyStr = "eh_aggregate_type"
 	commandTypeKeyStr   = "eh_command_type"
@@ -39,9 +35,6 @@ const (
 
 func init() {
 	RegisterContextMarshaler(func(ctx context.Context, vals map[string]interface{}) {
-		if ns, ok := ctx.Value(namespaceKey).(string); ok {
-			vals[namespaceKeyStr] = ns
-		}
 		if aggregateID, ok := AggregateIDFromContext(ctx); ok {
 			vals[aggregateIDKeyStr] = aggregateID.String()
 		}
@@ -53,9 +46,6 @@ func init() {
 		}
 	})
 	RegisterContextUnmarshaler(func(ctx context.Context, vals map[string]interface{}) context.Context {
-		if ns, ok := vals[namespaceKeyStr].(string); ok {
-			ctx = NewContextWithNamespace(ctx, ns)
-		}
 		if aggregateIDStr, ok := vals[aggregateIDKeyStr].(string); ok {
 			if aggregateID, err := uuid.Parse(aggregateIDStr); err == nil {
 				ctx = NewContextWithAggregateID(ctx, aggregateID)
@@ -74,8 +64,7 @@ func init() {
 type contextKey int
 
 const (
-	namespaceKey contextKey = iota
-	aggregateIDKey
+	aggregateIDKey contextKey = iota
 	aggregateTypeKey
 	commandTypeKey
 )
@@ -113,21 +102,6 @@ func NewContextWithCommandType(ctx context.Context, commandType CommandType) con
 	return context.WithValue(ctx, commandTypeKey, commandType)
 }
 
-// NamespaceFromContext returns the namespace from the context, or the default
-// namespace.
-func NamespaceFromContext(ctx context.Context) string {
-	if ns, ok := ctx.Value(namespaceKey).(string); ok {
-		return ns
-	}
-	return DefaultNamespace
-}
-
-// NewContextWithNamespace sets the namespace to use in the context. The
-// namespace is used to determine which database.
-func NewContextWithNamespace(ctx context.Context, namespace string) context.Context {
-	return context.WithValue(ctx, namespaceKey, namespace)
-}
-
 // Private context marshaling funcs.
 var (
 	contextMarshalFuncs   = []ContextMarshalFunc{}
diff --git a/eventhandler.go b/eventhandler.go
index a4df664..b286195 100644
--- a/eventhandler.go
+++ b/eventhandler.go
@@ -66,8 +66,6 @@ type CouldNotHandleEventError struct {
 	Err error
 	// Event is the event that failed to be handled.
 	Event Event
-	// Namespace is the namespace for the error.
-	Namespace string
 }
 
 // Error implements the Error method of the errors.Error interface.
@@ -76,11 +74,7 @@ func (e CouldNotHandleEventError) Error() string {
 	if e.Event != nil {
 		eventStr += " '" + e.Event.String() + "'"
 	}
-	errStr := fmt.Sprintf("could not handle event%s: %s", eventStr, e.Err)
-	if e.Namespace != "" {
-		errStr += " (" + e.Namespace + ")"
-	}
-	return errStr
+	return fmt.Sprintf("could not handle event%s: %s", eventStr, e.Err)
 }
 
 // Unwrap implements the errors.Unwrap method.
diff --git a/eventhandler/projector/eventhandler.go b/eventhandler/projector/eventhandler.go
index 9a86e89..b1e969d 100644
--- a/eventhandler/projector/eventhandler.go
+++ b/eventhandler/projector/eventhandler.go
@@ -51,14 +51,12 @@ func (t Type) String() string {
 	return string(t)
 }
 
-// Error is an error in the projector, with the namespace.
+// Error is an error in the projector.
 type Error struct {
 	// Err is the error that happened when projecting the event.
 	Err error
 	// Projector is the projector where the error happened.
 	Projector string
-	// Namespace is the namespace for the error.
-	Namespace string
 	// EventVersion is the version of the event.
 	EventVersion int
 	// EntityVersion is the version of the entity.
@@ -67,8 +65,8 @@ type Error struct {
 
 // Error implements the Error method of the errors.Error interface.
 func (e Error) Error() string {
-	return fmt.Sprintf("%s: %s, event: v%d, entity: v%d (%s)",
-		e.Projector, e.Err, e.EventVersion, e.EntityVersion, e.Namespace)
+	return fmt.Sprintf("%s: %s, event: v%d, entity: v%d",
+		e.Projector, e.Err, e.EventVersion, e.EntityVersion)
 }
 
 // Unwrap implements the errors.Unwrap method.
@@ -129,7 +127,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 			return Error{
 				Err:          ErrModelNotSet,
 				Projector:    h.projector.ProjectorType().String(),
-				Namespace:    eh.NamespaceFromContext(ctx),
 				EventVersion: event.Version(),
 			}
 		}
@@ -138,7 +135,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 		return Error{
 			Err:          err,
 			Projector:    h.projector.ProjectorType().String(),
-			Namespace:    eh.NamespaceFromContext(ctx),
 			EventVersion: event.Version(),
 		}
 	}
@@ -157,7 +153,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 			return Error{
 				Err:           eh.ErrIncorrectEntityVersion,
 				Projector:     h.projector.ProjectorType().String(),
-				Namespace:     eh.NamespaceFromContext(ctx),
 				EventVersion:  event.Version(),
 				EntityVersion: entityVersion,
 			}
@@ -170,7 +165,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 		return Error{
 			Err:           err,
 			Projector:     h.projector.ProjectorType().String(),
-			Namespace:     eh.NamespaceFromContext(ctx),
 			EventVersion:  event.Version(),
 			EntityVersion: entityVersion,
 		}
@@ -183,7 +177,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 			return Error{
 				Err:           eh.ErrIncorrectEntityVersion,
 				Projector:     h.projector.ProjectorType().String(),
-				Namespace:     eh.NamespaceFromContext(ctx),
 				EventVersion:  event.Version(),
 				EntityVersion: entityVersion,
 			}
@@ -196,7 +189,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 			return Error{
 				Err:           err,
 				Projector:     h.projector.ProjectorType().String(),
-				Namespace:     eh.NamespaceFromContext(ctx),
 				EventVersion:  event.Version(),
 				EntityVersion: entityVersion,
 			}
@@ -206,7 +198,6 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 			return Error{
 				Err:           err,
 				Projector:     h.projector.ProjectorType().String(),
-				Namespace:     eh.NamespaceFromContext(ctx),
 				EventVersion:  event.Version(),
 				EntityVersion: entityVersion,
 			}
diff --git a/eventhandler/saga/eventhandler.go b/eventhandler/saga/eventhandler.go
index e129ece..9a329f1 100644
--- a/eventhandler/saga/eventhandler.go
+++ b/eventhandler/saga/eventhandler.go
@@ -49,20 +49,17 @@ func (t Type) String() string {
 	return string(t)
 }
 
-// Error is an error in the projector, with the namespace.
+// Error is an error in the projector.
 type Error struct {
 	// Err is the error that happened when projecting the event.
 	Err error
 	// Saga is the saga where the error happened.
 	Saga string
-	// Namespace is the namespace for the error.
-	Namespace string
 }
 
 // Error implements the Error method of the errors.Error interface.
 func (e Error) Error() string {
-	return fmt.Sprintf("%s: %s (%s)",
-		e.Saga, e.Err, e.Namespace)
+	return fmt.Sprintf("%s: %s", e.Saga, e.Err)
 }
 
 // Unwrap implements the errors.Unwrap method.
@@ -93,9 +90,8 @@ func (h *EventHandler) HandleEvent(ctx context.Context, event eh.Event) error {
 	// Run the saga which can issue commands on the provided command handler.
 	if err := h.saga.RunSaga(ctx, event, h.commandHandler); err != nil {
 		return Error{
-			Err:       err,
-			Saga:      h.saga.SagaType().String(),
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:  err,
+			Saga: h.saga.SagaType().String(),
 		}
 	}
 
diff --git a/eventstore.go b/eventstore.go
index 1a5593b..ce2b282 100644
--- a/eventstore.go
+++ b/eventstore.go
@@ -30,14 +30,12 @@ type EventStore interface {
 	Load(context.Context, uuid.UUID) ([]Event, error)
 }
 
-// EventStoreError is an error in the event store, with the namespace.
+// EventStoreError is an error in the event store.
 type EventStoreError struct {
 	// Err is the error.
 	Err error
 	// BaseErr is an optional underlying error, for example from the DB driver.
 	BaseErr error
-	// Namespace is the namespace for the error.
-	Namespace string
 }
 
 // Error implements the Error method of the errors.Error interface.
@@ -46,7 +44,7 @@ func (e EventStoreError) Error() string {
 	if e.BaseErr != nil {
 		errStr += ": " + e.BaseErr.Error()
 	}
-	return errStr + " (" + e.Namespace + ")"
+	return errStr
 }
 
 // Unwrap implements the errors.Unwrap method.
diff --git a/eventstore/memory/eventmaintenance.go b/eventstore/memory/eventmaintenance.go
index b8de4cc..fd25c5f 100644
--- a/eventstore/memory/eventmaintenance.go
+++ b/eventstore/memory/eventmaintenance.go
@@ -23,11 +23,8 @@ import (
 
 // Replace implements the Replace method of the eventhorizon.EventStore interface.
 func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
-	// Ensure that the namespace exists.
-	ns := s.namespace(ctx)
-
 	s.dbMu.RLock()
-	aggregate, ok := s.db[ns][event.AggregateID()]
+	aggregate, ok := s.db[event.AggregateID()]
 	if !ok {
 		s.dbMu.RUnlock()
 		return eh.ErrAggregateNotFound
@@ -62,14 +59,11 @@ func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
 
 // RenameEvent implements the RenameEvent method of the eventhorizon.EventStore interface.
 func (s *EventStore) RenameEvent(ctx context.Context, from, to eh.EventType) error {
-	// Ensure that the namespace exists.
-	ns := s.namespace(ctx)
-
 	s.dbMu.Lock()
 	defer s.dbMu.Unlock()
 
 	updated := map[uuid.UUID]aggregateRecord{}
-	for id, aggregate := range s.db[ns] {
+	for id, aggregate := range s.db {
 		events := make([]eh.Event, len(aggregate.Events))
 		for i, e := range aggregate.Events {
 			if e.EventType() == from {
@@ -92,7 +86,7 @@ func (s *EventStore) RenameEvent(ctx context.Context, from, to eh.EventType) err
 	}
 
 	for id, aggregate := range updated {
-		s.db[ns][id] = aggregate
+		s.db[id] = aggregate
 	}
 
 	return nil
diff --git a/eventstore/memory/eventstore.go b/eventstore/memory/eventstore.go
index f6192b0..283ce07 100644
--- a/eventstore/memory/eventstore.go
+++ b/eventstore/memory/eventstore.go
@@ -28,8 +28,7 @@ import (
 // EventStore is an eventhorizon.EventStore where all events are stored in
 // memory and not persisted. Useful for testing and experimenting.
 type EventStore struct {
-	// The outer map is with namespace as key, the inner with aggregate ID.
-	db           map[string]map[uuid.UUID]aggregateRecord
+	db           map[uuid.UUID]aggregateRecord
 	dbMu         sync.RWMutex
 	eventHandler eh.EventHandler
 }
@@ -37,7 +36,7 @@ type EventStore struct {
 // NewEventStore creates a new EventStore using memory as storage.
 func NewEventStore(options ...Option) (*EventStore, error) {
 	s := &EventStore{
-		db: map[string]map[uuid.UUID]aggregateRecord{},
+		db: map[uuid.UUID]aggregateRecord{},
 	}
 	for _, option := range options {
 		if err := option(s); err != nil {
@@ -63,8 +62,7 @@ func WithEventHandler(h eh.EventHandler) Option {
 func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersion int) error {
 	if len(events) == 0 {
 		return eh.EventStoreError{
-			Err:       eh.ErrNoEventsToAppend,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrNoEventsToAppend,
 		}
 	}
 
@@ -76,16 +74,14 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		// Only accept events belonging to the same aggregate.
 		if event.AggregateID() != aggregateID {
 			return eh.EventStoreError{
-				Err:       eh.ErrInvalidEvent,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: eh.ErrInvalidEvent,
 			}
 		}
 
 		// Only accept events that apply to the correct aggregate version.
 		if event.Version() != originalVersion+i+1 {
 			return eh.EventStoreError{
-				Err:       eh.ErrIncorrectEventVersion,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: eh.ErrIncorrectEventVersion,
 			}
 		}
 
@@ -97,8 +93,6 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		dbEvents[i] = e
 	}
 
-	ns := s.namespace(ctx)
-
 	s.dbMu.Lock()
 	defer s.dbMu.Unlock()
 
@@ -110,24 +104,23 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 			Events:      dbEvents,
 		}
 
-		s.db[ns][aggregateID] = aggregate
+		s.db[aggregateID] = aggregate
 	} else {
 		// Increment aggregate version on insert of new event record, and
 		// only insert if version of aggregate is matching (ie not changed
 		// since loading the aggregate).
-		if aggregate, ok := s.db[ns][aggregateID]; ok {
+		if aggregate, ok := s.db[aggregateID]; ok {
 			if aggregate.Version != originalVersion {
 				return eh.EventStoreError{
-					Err:       eh.ErrCouldNotSaveEvents,
-					BaseErr:   fmt.Errorf("invalid original version %d", originalVersion),
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err:     eh.ErrCouldNotSaveEvents,
+					BaseErr: fmt.Errorf("invalid original version %d", originalVersion),
 				}
 			}
 
 			aggregate.Version += len(dbEvents)
 			aggregate.Events = append(aggregate.Events, dbEvents...)
 
-			s.db[ns][aggregateID] = aggregate
+			s.db[aggregateID] = aggregate
 		}
 	}
 
@@ -137,9 +130,8 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		for _, e := range events {
 			if err := s.eventHandler.HandleEvent(ctx, e); err != nil {
 				return eh.CouldNotHandleEventError{
-					Err:       err,
-					Event:     e,
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err:   err,
+					Event: e,
 				}
 			}
 		}
@@ -153,12 +145,7 @@ func (s *EventStore) Load(ctx context.Context, id uuid.UUID) ([]eh.Event, error)
 	s.dbMu.RLock()
 	defer s.dbMu.RUnlock()
 
-	// Ensure that the namespace exists.
-	s.dbMu.RUnlock()
-	ns := s.namespace(ctx)
-	s.dbMu.RLock()
-
-	aggregate, ok := s.db[ns][id]
+	aggregate, ok := s.db[id]
 	if !ok {
 		return []eh.Event{}, nil
 	}
@@ -175,17 +162,6 @@ func (s *EventStore) Load(ctx context.Context, id uuid.UUID) ([]eh.Event, error)
 	return events, nil
 }
 
-// Helper to get the namespace and ensure that its data exists.
-func (s *EventStore) namespace(ctx context.Context) string {
-	s.dbMu.Lock()
-	defer s.dbMu.Unlock()
-	ns := eh.NamespaceFromContext(ctx)
-	if _, ok := s.db[ns]; !ok {
-		s.db[ns] = map[uuid.UUID]aggregateRecord{}
-	}
-	return ns
-}
-
 type aggregateRecord struct {
 	AggregateID uuid.UUID
 	Version     int
@@ -201,8 +177,7 @@ func copyEvent(ctx context.Context, event eh.Event) (eh.Event, error) {
 		var err error
 		if data, err = eh.CreateEventData(event.EventType()); err != nil {
 			return nil, eh.EventStoreError{
-				Err:       fmt.Errorf("could not create event data: %w", err),
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: fmt.Errorf("could not create event data: %w", err),
 			}
 		}
 		copier.Copy(data, event.Data())
diff --git a/eventstore/mongodb/eventmaintenance.go b/eventstore/mongodb/eventmaintenance.go
index d2c7991..4bb4cc2 100644
--- a/eventstore/mongodb/eventmaintenance.go
+++ b/eventstore/mongodb/eventmaintenance.go
@@ -28,16 +28,13 @@ import (
 
 // Replace implements the Replace method of the eventhorizon.EventStore interface.
 func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
-	c := s.client.Database(s.dbName(ctx)).Collection("events")
-
 	// First check if the aggregate exists, the not found error in the update
 	// query can mean both that the aggregate or the event is not found.
-	if n, err := c.CountDocuments(ctx, bson.M{"_id": event.AggregateID()}); n == 0 {
+	if n, err := s.aggregates.CountDocuments(ctx, bson.M{"_id": event.AggregateID()}); n == 0 {
 		return eh.ErrAggregateNotFound
 	} else if err != nil {
 		return eh.EventStoreError{
-			Err:       err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: err,
 		}
 	}
 
@@ -48,7 +45,7 @@ func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
 	}
 
 	// Find and replace the event.
-	if r, err := c.UpdateOne(ctx,
+	if r, err := s.aggregates.UpdateOne(ctx,
 		bson.M{
 			"_id":            event.AggregateID(),
 			"events.version": event.Version(),
@@ -58,9 +55,8 @@ func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
 		},
 	); err != nil {
 		return eh.EventStoreError{
-			Err:       eh.ErrCouldNotSaveEvents,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEvents,
+			BaseErr: err,
 		}
 	} else if r.MatchedCount == 0 {
 		return eh.ErrInvalidEvent
@@ -71,11 +67,9 @@ func (s *EventStore) Replace(ctx context.Context, event eh.Event) error {
 
 // RenameEvent implements the RenameEvent method of the eventhorizon.EventStore interface.
 func (s *EventStore) RenameEvent(ctx context.Context, from, to eh.EventType) error {
-	c := s.client.Database(s.dbName(ctx)).Collection("events")
-
 	// Find and rename all events.
 	// TODO: Maybe use change info.
-	if _, err := c.UpdateMany(ctx,
+	if _, err := s.aggregates.UpdateMany(ctx,
 		bson.M{
 			"events.event_type": from.String(),
 		},
@@ -84,9 +78,8 @@ func (s *EventStore) RenameEvent(ctx context.Context, from, to eh.EventType) err
 		},
 	); err != nil {
 		return eh.EventStoreError{
-			Err:       eh.ErrCouldNotSaveEvents,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEvents,
+			BaseErr: err,
 		}
 	}
 
@@ -95,12 +88,9 @@ func (s *EventStore) RenameEvent(ctx context.Context, from, to eh.EventType) err
 
 // Clear clears the event storage.
 func (s *EventStore) Clear(ctx context.Context) error {
-	c := s.client.Database(s.dbName(ctx)).Collection("events")
-
-	if err := c.Drop(ctx); err != nil {
+	if err := s.aggregates.Drop(ctx); err != nil {
 		return eh.EventStoreError{
-			Err:       fmt.Errorf("could not clear collection: %w", err),
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: fmt.Errorf("could not clear collection: %w", err),
 		}
 	}
 	return nil
diff --git a/eventstore/mongodb/eventstore.go b/eventstore/mongodb/eventstore.go
index 4a1ba50..7323036 100644
--- a/eventstore/mongodb/eventstore.go
+++ b/eventstore/mongodb/eventstore.go
@@ -38,13 +38,12 @@ import (
 // as values.
 type EventStore struct {
 	client       *mongo.Client
-	dbPrefix     string
-	dbName       func(ctx context.Context) string
+	aggregates   *mongo.Collection
 	eventHandler eh.EventHandler
 }
 
 // NewEventStore creates a new EventStore with a MongoDB URI: `mongodb://hostname`.
-func NewEventStore(uri, dbPrefix string, options ...Option) (*EventStore, error) {
+func NewEventStore(uri, db string, options ...Option) (*EventStore, error) {
 	opts := mongoOptions.Client().ApplyURI(uri)
 	opts.SetWriteConcern(writeconcern.New(writeconcern.WMajority()))
 	opts.SetReadConcern(readconcern.Majority())
@@ -54,24 +53,18 @@ func NewEventStore(uri, dbPrefix string, options ...Option) (*EventStore, error)
 		return nil, fmt.Errorf("could not connect to DB: %w", err)
 	}
 
-	return NewEventStoreWithClient(client, dbPrefix, options...)
+	return NewEventStoreWithClient(client, db, options...)
 }
 
 // NewEventStoreWithClient creates a new EventStore with a client.
-func NewEventStoreWithClient(client *mongo.Client, dbPrefix string, options ...Option) (*EventStore, error) {
+func NewEventStoreWithClient(client *mongo.Client, db string, options ...Option) (*EventStore, error) {
 	if client == nil {
 		return nil, fmt.Errorf("missing DB client")
 	}
 
 	s := &EventStore{
-		client:   client,
-		dbPrefix: dbPrefix,
-	}
-
-	// Use the a prefix and namespace from the context for DB name.
-	s.dbName = func(ctx context.Context) string {
-		ns := eh.NamespaceFromContext(ctx)
-		return dbPrefix + "_" + ns
+		client:     client,
+		aggregates: client.Database(db).Collection("events"),
 	}
 
 	for _, option := range options {
@@ -86,24 +79,6 @@ func NewEventStoreWithClient(client *mongo.Client, dbPrefix string, options ...O
 // Option is an option setter used to configure creation.
 type Option func(*EventStore) error
 
-// WithPrefixAsDBName uses only the prefix as DB name, without namespace support.
-func WithPrefixAsDBName() Option {
-	return func(s *EventStore) error {
-		s.dbName = func(context.Context) string {
-			return s.dbPrefix
-		}
-		return nil
-	}
-}
-
-// WithDBName uses a custom DB name function.
-func WithDBName(dbName func(context.Context) string) Option {
-	return func(s *EventStore) error {
-		s.dbName = dbName
-		return nil
-	}
-}
-
 // WithEventHandler adds an event handler that will be called when saving events.
 // An example would be to add an event bus to publish events.
 func WithEventHandler(h eh.EventHandler) Option {
@@ -117,8 +92,7 @@ func WithEventHandler(h eh.EventHandler) Option {
 func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersion int) error {
 	if len(events) == 0 {
 		return eh.EventStoreError{
-			Err:       eh.ErrNoEventsToAppend,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrNoEventsToAppend,
 		}
 	}
 
@@ -130,16 +104,14 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		// Only accept events belonging to the same aggregate.
 		if event.AggregateID() != aggregateID {
 			return eh.EventStoreError{
-				Err:       eh.ErrInvalidEvent,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: eh.ErrInvalidEvent,
 			}
 		}
 
 		// Only accept events that apply to the correct aggregate version.
 		if event.Version() != originalVersion+i+1 {
 			return eh.EventStoreError{
-				Err:       eh.ErrIncorrectEventVersion,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: eh.ErrIncorrectEventVersion,
 			}
 		}
 
@@ -151,8 +123,6 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		dbEvents[i] = *e
 	}
 
-	c := s.client.Database(s.dbName(ctx)).Collection("events")
-
 	// Either insert a new aggregate or append to an existing.
 	if originalVersion == 0 {
 		aggregate := aggregateRecord{
@@ -161,18 +131,17 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 			Events:      dbEvents,
 		}
 
-		if _, err := c.InsertOne(ctx, aggregate); err != nil {
+		if _, err := s.aggregates.InsertOne(ctx, aggregate); err != nil {
 			return eh.EventStoreError{
-				Err:       eh.ErrCouldNotSaveEvents,
-				BaseErr:   err,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err:     eh.ErrCouldNotSaveEvents,
+				BaseErr: err,
 			}
 		}
 	} else {
 		// Increment aggregate version on insert of new event record, and
 		// only insert if version of aggregate is matching (ie not changed
 		// since loading the aggregate).
-		if r, err := c.UpdateOne(ctx,
+		if r, err := s.aggregates.UpdateOne(ctx,
 			bson.M{
 				"_id":     aggregateID,
 				"version": originalVersion,
@@ -183,15 +152,13 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 			},
 		); err != nil {
 			return eh.EventStoreError{
-				Err:       eh.ErrCouldNotSaveEvents,
-				BaseErr:   err,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err:     eh.ErrCouldNotSaveEvents,
+				BaseErr: err,
 			}
 		} else if r.MatchedCount == 0 {
 			return eh.EventStoreError{
-				Err:       eh.ErrCouldNotSaveEvents,
-				BaseErr:   fmt.Errorf("invalid original version %d", originalVersion),
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err:     eh.ErrCouldNotSaveEvents,
+				BaseErr: fmt.Errorf("invalid original version %d", originalVersion),
 			}
 		}
 	}
@@ -201,9 +168,8 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 		for _, e := range events {
 			if err := s.eventHandler.HandleEvent(ctx, e); err != nil {
 				return eh.CouldNotHandleEventError{
-					Err:       err,
-					Event:     e,
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err:   err,
+					Event: e,
 				}
 			}
 		}
@@ -214,16 +180,13 @@ func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersio
 
 // Load implements the Load method of the eventhorizon.EventStore interface.
 func (s *EventStore) Load(ctx context.Context, id uuid.UUID) ([]eh.Event, error) {
-	c := s.client.Database(s.dbName(ctx)).Collection("events")
-
 	var aggregate aggregateRecord
-	err := c.FindOne(ctx, bson.M{"_id": id}).Decode(&aggregate)
+	err := s.aggregates.FindOne(ctx, bson.M{"_id": id}).Decode(&aggregate)
 	if err == mongo.ErrNoDocuments {
 		return []eh.Event{}, nil
 	} else if err != nil {
 		return nil, eh.EventStoreError{
-			Err:       fmt.Errorf("could not find event: %w", err),
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: fmt.Errorf("could not find event: %w", err),
 		}
 	}
 
@@ -234,14 +197,12 @@ func (s *EventStore) Load(ctx context.Context, id uuid.UUID) ([]eh.Event, error)
 			var err error
 			if e.data, err = eh.CreateEventData(e.EventType); err != nil {
 				return nil, eh.EventStoreError{
-					Err:       fmt.Errorf("could not create event data: %w", err),
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err: fmt.Errorf("could not create event data: %w", err),
 				}
 			}
 			if err := bson.Unmarshal(e.RawData, e.data); err != nil {
 				return nil, eh.EventStoreError{
-					Err:       fmt.Errorf("could not unmarshal event data: %w", err),
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err: fmt.Errorf("could not unmarshal event data: %w", err),
 				}
 			}
 			e.RawData = nil
@@ -311,8 +272,7 @@ func newEvt(ctx context.Context, event eh.Event) (*evt, error) {
 		e.RawData, err = bson.Marshal(event.Data())
 		if err != nil {
 			return nil, eh.EventStoreError{
-				Err:       fmt.Errorf("could not marshal event data: %w", err),
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err: fmt.Errorf("could not marshal event data: %w", err),
 			}
 		}
 	}
diff --git a/eventstore/mongodb_v2/eventstore.go b/eventstore/mongodb_v2/eventstore.go
index 2875244..a3e1d97 100644
--- a/eventstore/mongodb_v2/eventstore.go
+++ b/eventstore/mongodb_v2/eventstore.go
@@ -36,7 +36,6 @@ import (
 // EventStore is an eventhorizon.EventStore for MongoDB, using one collection
 // for all events and another to keep track of all aggregates/streams. It also
 // keep tracks of the global position of events, stored as metadata.
-// NOTE: This event store has NO namespace support.
 type EventStore struct {
 	client       *mongo.Client
 	events       *mongo.Collection
diff --git a/examples/todomvc/backend/domains/todo/setup.go b/examples/todomvc/backend/domains/todo/setup.go
index cb24c68..3d631e9 100644
--- a/examples/todomvc/backend/domains/todo/setup.go
+++ b/examples/todomvc/backend/domains/todo/setup.go
@@ -23,11 +23,11 @@ func SetupDomain(
 ) error {
 
 	// Set the entity factory for the base repo.
-	if repo := memory.Repository(repo); repo != nil {
-		repo.SetEntityFactory(func() eh.Entity { return &TodoList{} })
+	if memoryRepo := memory.IntoRepo(ctx, repo); memoryRepo != nil {
+		memoryRepo.SetEntityFactory(func() eh.Entity { return &TodoList{} })
 	}
-	if repo := mongodb.Repository(repo); repo != nil {
-		repo.SetEntityFactory(func() eh.Entity { return &TodoList{} })
+	if mongodbRepo := mongodb.IntoRepo(ctx, repo); mongodbRepo != nil {
+		mongodbRepo.SetEntityFactory(func() eh.Entity { return &TodoList{} })
 	}
 
 	// Create the read model projector.
diff --git a/mocks/mocks.go b/mocks/mocks.go
index 7fcfbfe..49a1d99 100644
--- a/mocks/mocks.go
+++ b/mocks/mocks.go
@@ -381,8 +381,8 @@ type Repo struct {
 
 var _ = eh.ReadWriteRepo(&Repo{})
 
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
 	return r.ParentRepo
 }
 
diff --git a/namespace/context.go b/namespace/context.go
new file mode 100644
index 0000000..dc79a43
--- /dev/null
+++ b/namespace/context.go
@@ -0,0 +1,63 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+
+	eh "github.com/looplab/eventhorizon"
+)
+
+// DefaultNamespace is the namespace to use if not set in the context.
+const DefaultNamespace = "default"
+
+// Strings used to marshal context values.
+const (
+	namespaceKeyStr = "eh_namespace"
+)
+
+func init() {
+	eh.RegisterContextMarshaler(func(ctx context.Context, vals map[string]interface{}) {
+		if ns, ok := ctx.Value(namespaceKey).(string); ok {
+			vals[namespaceKeyStr] = ns
+		}
+	})
+	eh.RegisterContextUnmarshaler(func(ctx context.Context, vals map[string]interface{}) context.Context {
+		if ns, ok := vals[namespaceKeyStr].(string); ok {
+			ctx = NewContext(ctx, ns)
+		}
+		return ctx
+	})
+}
+
+type contextKey int
+
+const (
+	namespaceKey contextKey = iota
+)
+
+// FromContext returns the namespace from the context, or the default namespace.
+func FromContext(ctx context.Context) string {
+	if ns, ok := ctx.Value(namespaceKey).(string); ok {
+		return ns
+	}
+	return DefaultNamespace
+}
+
+// NewContext sets the namespace to use in the context. The namespace is used to
+// determine which database to use, among other things.
+func NewContext(ctx context.Context, namespace string) context.Context {
+	return context.WithValue(ctx, namespaceKey, namespace)
+}
diff --git a/namespace/error.go b/namespace/error.go
new file mode 100644
index 0000000..3e022bc
--- /dev/null
+++ b/namespace/error.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import "fmt"
+
+// Error is an error that contains info about in which name space the underlying
+// error occurred.
+type Error struct {
+	// Err is the error.
+	Err error
+	// Namespace is the namespace for the error.
+	Namespace string
+}
+
+// Error implements the Error method of the errors.Error interface.
+func (e Error) Error() string {
+	return fmt.Sprintf("%s (%s)", e.Err, e.Namespace)
+}
+
+// Unwrap implements the errors.Unwrap method.
+func (e Error) Unwrap() error {
+	return e.Err
+}
+
+// Cause implements the github.com/pkg/errors Unwrap method.
+func (e Error) Cause() error {
+	return e.Unwrap()
+}
diff --git a/namespace/eventstore.go b/namespace/eventstore.go
new file mode 100644
index 0000000..cf032d8
--- /dev/null
+++ b/namespace/eventstore.go
@@ -0,0 +1,102 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	eh "github.com/looplab/eventhorizon"
+	"github.com/looplab/eventhorizon/uuid"
+)
+
+// EventStore is an event store with support for namespaces passed in the context.
+type EventStore struct {
+	eventStores   map[string]eh.EventStore
+	eventStoresMu sync.RWMutex
+	newEventStore func(ns string) (eh.EventStore, error)
+}
+
+// NewEventStore creates a new event store which will use the provided factory
+// function to create new event stores for the provided namespace.
+//
+// Usage:
+//    eventStore := NewEventStore(func(ns string) (eh.EventStore, error) {
+//        s, err := mongodb.NewEventStore("mongodb://", ns)
+//        if err != nil {
+//            return nil, err
+//        }
+//        return s, nil
+//    })
+//
+// Usage shared DB client:
+//    client, err := mongo.Connect(ctx)
+//    ...
+//
+//    eventStore := NewEventStore(func(ns string) (eh.EventStore, error) {
+//        s, err := mongodb.NewEventStoreWithClient(client, ns)
+//        if err != nil {
+//            return nil, err
+//        }
+//        return s, nil
+//    })
+func NewEventStore(factory func(ns string) (eh.EventStore, error)) *EventStore {
+	return &EventStore{
+		eventStores:   map[string]eh.EventStore{},
+		newEventStore: factory,
+	}
+}
+
+// Save implements the Save method of the eventhorizon.EventStore interface.
+func (s *EventStore) Save(ctx context.Context, events []eh.Event, originalVersion int) error {
+	store, err := s.eventStore(ctx)
+	if err != nil {
+		return err
+	}
+	return store.Save(ctx, events, originalVersion)
+}
+
+// Load implements the Load method of the eventhorizon.EventStore interface.
+func (s *EventStore) Load(ctx context.Context, id uuid.UUID) ([]eh.Event, error) {
+	store, err := s.eventStore(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return store.Load(ctx, id)
+}
+
+// eventStore is a helper that returns or creates event stores for each namespace.
+func (s *EventStore) eventStore(ctx context.Context) (eh.EventStore, error) {
+	ns := FromContext(ctx)
+	s.eventStoresMu.RLock()
+	eventStore, ok := s.eventStores[ns]
+	s.eventStoresMu.RUnlock()
+	if !ok {
+		s.eventStoresMu.Lock()
+		// Perform an additional existence check within the write lock in the
+		// unlikely event that someone else added the event store right before us.
+		if _, ok := s.eventStores[ns]; !ok {
+			var err error
+			eventStore, err = s.newEventStore(ns)
+			if err != nil {
+				return nil, fmt.Errorf("could not create event store for namespace '%s': %w", ns, err)
+			}
+			s.eventStores[ns] = eventStore
+		}
+		s.eventStoresMu.Unlock()
+	}
+	return eventStore, nil
+}
diff --git a/namespace/repo.go b/namespace/repo.go
new file mode 100644
index 0000000..e0fab68
--- /dev/null
+++ b/namespace/repo.go
@@ -0,0 +1,148 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	eh "github.com/looplab/eventhorizon"
+	"github.com/looplab/eventhorizon/uuid"
+)
+
+// Repo is a repo with support for namespaces passed in the context.
+type Repo struct {
+	repos   map[string]eh.ReadWriteRepo
+	reposMu sync.RWMutex
+	newRepo func(ns string) (eh.ReadWriteRepo, error)
+}
+
+// NewRepo creates a new repo which will use the provided factory function to
+// create new repos for the provided namespace.
+//
+// Usage:
+//    repo := NewRepo(func(ns string) (eh.ReadWriteRepo, error) {
+//        r, err := mongodb.NewRepo("mongodb://", "db", "model")
+//        if err != nil {
+//            return nil, err
+//        }
+//        r.SetEntityFactory(func() eh.Entity{
+//            return &Model{}
+//        })
+//        return r, nil
+//    })
+//
+// Usage shared DB client:
+//    client, err := mongo.Connect(ctx)
+//    ...
+//
+//    repo := NewRepo(func(ns string) (eh.ReadWriteRepo, error) {
+//        r, err := mongodb.NewRepoWithClient(client, "db", "model")
+//        if err != nil {
+//            return nil, err
+//        }
+//        r.SetEntityFactory(func() eh.Entity{
+//            return &Model{}
+//        })
+//        return r, nil
+//    })
+func NewRepo(factory func(ns string) (eh.ReadWriteRepo, error)) *Repo {
+	return &Repo{
+		repos:   map[string]eh.ReadWriteRepo{},
+		newRepo: factory,
+	}
+}
+
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
+	repo, err := r.repo(ctx)
+	if err != nil {
+		// TODO: Log/handle error.
+		return nil
+	}
+	return repo
+}
+
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
+		return nil
+	}
+	if r, ok := repo.(*Repo); ok {
+		return r
+	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
+}
+
+// Find implements the Find method of the eventhorizon.ReadRepo interface.
+func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
+	repo, err := r.repo(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return repo.Find(ctx, id)
+}
+
+// FindAll implements the FindAll method of the eventhorizon.ReadRepo interface.
+func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
+	repo, err := r.repo(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return repo.FindAll(ctx)
+}
+
+// Save implements the Save method of the eventhorizon.WriteRepo interface.
+func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
+	repo, err := r.repo(ctx)
+	if err != nil {
+		return err
+	}
+	return repo.Save(ctx, entity)
+}
+
+// Remove implements the Remove method of the eventhorizon.WriteRepo interface.
+func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
+	repo, err := r.repo(ctx)
+	if err != nil {
+		return err
+	}
+	return repo.Remove(ctx, id)
+}
+
+// repo is a helper that returns or creates repos for each namespace.
+func (r *Repo) repo(ctx context.Context) (eh.ReadWriteRepo, error) {
+	ns := FromContext(ctx)
+	r.reposMu.RLock()
+	repo, ok := r.repos[ns]
+	r.reposMu.RUnlock()
+	if !ok {
+		r.reposMu.Lock()
+		// Perform an additional existence check within the write lock in the
+		// unlikely event that someone else added the repo right before us.
+		if _, ok := r.repos[ns]; !ok {
+			var err error
+			repo, err = r.newRepo(ns)
+			if err != nil {
+				return nil, fmt.Errorf("could not create repo for namespace '%s': %w", ns, err)
+			}
+			r.repos[ns] = repo
+		}
+		r.reposMu.Unlock()
+	}
+	return repo, nil
+}
diff --git a/repo.go b/repo.go
index 340978c..21aa4fa 100644
--- a/repo.go
+++ b/repo.go
@@ -23,9 +23,9 @@ import (
 
 // ReadRepo is a read repository for entities.
 type ReadRepo interface {
-	// Parent returns the parent read repository, if there is one.
+	// InnerRepo returns the inner read repository, if there is one.
 	// Useful for iterating a wrapped set of repositories to get a specific one.
-	Parent() ReadRepo
+	InnerRepo(context.Context) ReadRepo
 
 	// Find returns an entity for an ID.
 	Find(context.Context, uuid.UUID) (Entity, error)
@@ -84,14 +84,12 @@ var (
 	ErrIncorrectEntityVersion = errors.New("incorrect entity version")
 )
 
-// RepoError is an error in the read repository, with the namespace.
+// RepoError is an error in the read repository.
 type RepoError struct {
 	// Err is the error.
 	Err error
 	// BaseErr is an optional underlying error, for example from the DB driver.
 	BaseErr error
-	// Namespace is the namespace for the error.
-	Namespace string
 }
 
 // Error implements the Error method of the errors.Error interface.
@@ -100,7 +98,7 @@ func (e RepoError) Error() string {
 	if e.BaseErr != nil {
 		errStr += ": " + e.BaseErr.Error()
 	}
-	return errStr + " (" + e.Namespace + ")"
+	return errStr
 }
 
 // Unwrap implements the errors.Unwrap method.
diff --git a/repo/cache/repo.go b/repo/cache/repo.go
index 5d45067..023bc46 100644
--- a/repo/cache/repo.go
+++ b/repo/cache/repo.go
@@ -23,8 +23,6 @@ import (
 	"github.com/looplab/eventhorizon/uuid"
 )
 
-type namespace string
-
 // Repo is a middleware that adds caching to a read repository. It will update
 // the cache when it receives events affecting the cached items. The primary
 // purpose is to use it with smaller collections accessed often.
@@ -32,7 +30,7 @@ type namespace string
 type Repo struct {
 	eh.ReadWriteRepo
 
-	cache   map[namespace]map[uuid.UUID]eh.Entity
+	cache   map[uuid.UUID]eh.Entity
 	cacheMu sync.RWMutex
 }
 
@@ -40,8 +38,25 @@ type Repo struct {
 func NewRepo(repo eh.ReadWriteRepo) *Repo {
 	return &Repo{
 		ReadWriteRepo: repo,
-		cache:         map[namespace]map[uuid.UUID]eh.Entity{},
+		cache:         map[uuid.UUID]eh.Entity{},
+	}
+}
+
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
+	return r.ReadWriteRepo
+}
+
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
+		return nil
+	}
+	if r, ok := repo.(*Repo); ok {
+		return r
 	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
 }
 
 // HandlerType implements the HandlerType method of the eventhorizon.EventHandler interface.
@@ -54,25 +69,17 @@ func (r *Repo) HandlerType() eh.EventHandlerType {
 // The repo should be added with a eh.MatchAny or eh.MatchAggregate for best
 // effect (depending on if the underlying repo is used for all or individual aggregate types).
 func (r *Repo) HandleEvent(ctx context.Context, event eh.Event) error {
-	ns := r.namespace(ctx)
 	r.cacheMu.Lock()
-	delete(r.cache[ns], event.AggregateID())
+	delete(r.cache, event.AggregateID())
 	r.cacheMu.Unlock()
 	return nil
 }
 
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
-	return r.ReadWriteRepo
-}
-
 // Find implements the Find method of the eventhorizon.ReadModel interface.
 func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
-	ns := r.namespace(ctx)
-
 	// First check the cache.
 	r.cacheMu.RLock()
-	entity, ok := r.cache[ns][id]
+	entity, ok := r.cache[id]
 	r.cacheMu.RUnlock()
 	if ok {
 		return entity, nil
@@ -84,7 +91,7 @@ func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 		return nil, err
 	}
 	r.cacheMu.Lock()
-	r.cache[ns][id] = entity
+	r.cache[id] = entity
 	r.cacheMu.Unlock()
 
 	return entity, nil
@@ -98,10 +105,9 @@ func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 	}
 
 	// Cache all items.
-	ns := r.namespace(ctx)
 	r.cacheMu.Lock()
 	for _, entity := range entities {
-		r.cache[ns][entity.EntityID()] = entity
+		r.cache[entity.EntityID()] = entity
 	}
 	r.cacheMu.Unlock()
 
@@ -111,9 +117,8 @@ func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 // Save implements the Save method of the eventhorizon.WriteRepo interface.
 func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 	// Bust the cache on save.
-	ns := r.namespace(ctx)
 	r.cacheMu.Lock()
-	delete(r.cache[ns], entity.EntityID())
+	delete(r.cache, entity.EntityID())
 	r.cacheMu.Unlock()
 
 	return r.ReadWriteRepo.Save(ctx, entity)
@@ -122,36 +127,9 @@ func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 // Remove implements the Remove method of the eventhorizon.WriteRepo interface.
 func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
 	// Bust the cache on remove.
-	ns := r.namespace(ctx)
 	r.cacheMu.Lock()
-	delete(r.cache[ns], id)
+	delete(r.cache, id)
 	r.cacheMu.Unlock()
 
 	return r.ReadWriteRepo.Remove(ctx, id)
 }
-
-// Helper to get the namespace and ensure that its data exists.
-func (r *Repo) namespace(ctx context.Context) namespace {
-	ns := namespace(eh.NamespaceFromContext(ctx))
-
-	r.cacheMu.Lock()
-	defer r.cacheMu.Unlock()
-	if _, ok := r.cache[ns]; !ok {
-		r.cache[ns] = map[uuid.UUID]eh.Entity{}
-	}
-
-	return ns
-}
-
-// Repository returns a parent ReadRepo if there is one.
-func Repository(repo eh.ReadRepo) *Repo {
-	if repo == nil {
-		return nil
-	}
-
-	if r, ok := repo.(*Repo); ok {
-		return r
-	}
-
-	return Repository(repo.Parent())
-}
diff --git a/repo/memory/repo.go b/repo/memory/repo.go
index 881a8d0..7a7644d 100644
--- a/repo/memory/repo.go
+++ b/repo/memory/repo.go
@@ -24,57 +24,60 @@ import (
 	"github.com/looplab/eventhorizon/uuid"
 )
 
-type namespace string
-
 // ErrModelNotSet is when an model factory is not set on the Repo.
 var ErrModelNotSet = errors.New("model not set")
 
 // Repo implements an in memory repository of read models.
 type Repo struct {
-	// The outer map is with namespace as key, the inner with aggregate ID.
-	db   map[namespace]map[uuid.UUID][]byte
+	db   map[uuid.UUID][]byte
 	dbMu sync.RWMutex
 
 	// A list of all item ids, only the order is used.
-	// The outer map is for the namespace.
-	ids       map[namespace][]uuid.UUID
+	ids       []uuid.UUID
 	factoryFn func() eh.Entity
 }
 
 // NewRepo creates a new Repo.
 func NewRepo() *Repo {
 	r := &Repo{
-		ids: map[namespace][]uuid.UUID{},
-		db:  map[namespace]map[uuid.UUID][]byte{},
+		db: map[uuid.UUID][]byte{},
 	}
 	return r
 }
 
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
 	return nil
 }
 
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
+		return nil
+	}
+	if r, ok := repo.(*Repo); ok {
+		return r
+	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
+}
+
 // Find implements the Find method of the eventhorizon.ReadRepo interface.
 func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 	if r.factoryFn == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	ns := r.namespace(ctx)
-
 	r.dbMu.RLock()
 	defer r.dbMu.RUnlock()
 
 	// Fetch entity.
-	b, ok := r.db[ns][id]
+	b, ok := r.db[id]
 	if !ok {
 		return nil, eh.RepoError{
-			Err:       eh.ErrEntityNotFound,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrEntityNotFound,
 		}
 	}
 
@@ -82,9 +85,8 @@ func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 	entity := r.factoryFn()
 	if err := json.Unmarshal(b, &entity); err != nil {
 		return nil, eh.RepoError{
-			Err:       eh.ErrCouldNotLoadEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotLoadEntity,
+			BaseErr: err,
 		}
 	}
 
@@ -95,24 +97,20 @@ func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 	if r.factoryFn == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	ns := r.namespace(ctx)
-
 	r.dbMu.RLock()
 	defer r.dbMu.RUnlock()
 	result := []eh.Entity{}
-	for _, id := range r.ids[ns] {
-		if b, ok := r.db[ns][id]; ok {
+	for _, id := range r.ids {
+		if b, ok := r.db[id]; ok {
 			entity := r.factoryFn()
 			if err := json.Unmarshal(b, &entity); err != nil {
 				return nil, eh.RepoError{
-					Err:       eh.ErrCouldNotLoadEntity,
-					BaseErr:   err,
-					Namespace: eh.NamespaceFromContext(ctx),
+					Err:     eh.ErrCouldNotLoadEntity,
+					BaseErr: err,
 				}
 			}
 			result = append(result, entity)
@@ -126,18 +124,14 @@ func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 	if r.factoryFn == nil {
 		return eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	ns := r.namespace(ctx)
-
 	if entity.EntityID() == uuid.Nil {
 		return eh.RepoError{
-			Err:       eh.ErrCouldNotSaveEntity,
-			BaseErr:   eh.ErrMissingEntityID,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEntity,
+			BaseErr: eh.ErrMissingEntityID,
 		}
 	}
 
@@ -149,45 +143,41 @@ func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 	b, err := json.Marshal(entity)
 	if err != nil {
 		return eh.RepoError{
-			Err:       eh.ErrCouldNotSaveEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEntity,
+			BaseErr: err,
 		}
 	}
 
 	// Update ID index if the item is new.
-	if _, ok := r.db[ns][id]; !ok {
-		r.ids[ns] = append(r.ids[ns], id)
+	if _, ok := r.db[id]; !ok {
+		r.ids = append(r.ids, id)
 	}
-	r.db[ns][id] = b
+	r.db[id] = b
 
 	return nil
 }
 
 // Remove implements the Remove method of the eventhorizon.WriteRepo interface.
 func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
-	ns := r.namespace(ctx)
-
 	r.dbMu.Lock()
 	defer r.dbMu.Unlock()
-	if _, ok := r.db[ns][id]; ok {
-		delete(r.db[ns], id)
+	if _, ok := r.db[id]; ok {
+		delete(r.db, id)
 
 		index := -1
-		for i, d := range r.ids[ns] {
+		for i, d := range r.ids {
 			if id == d {
 				index = i
 				break
 			}
 		}
-		r.ids[ns] = append(r.ids[ns][:index], r.ids[ns][index+1:]...)
+		r.ids = append(r.ids[:index], r.ids[index+1:]...)
 
 		return nil
 	}
 
 	return eh.RepoError{
-		Err:       eh.ErrEntityNotFound,
-		Namespace: eh.NamespaceFromContext(ctx),
+		Err: eh.ErrEntityNotFound,
 	}
 }
 
@@ -195,30 +185,3 @@ func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
 func (r *Repo) SetEntityFactory(f func() eh.Entity) {
 	r.factoryFn = f
 }
-
-// Helper to get the namespace and ensure that its data exists.
-func (r *Repo) namespace(ctx context.Context) namespace {
-	ns := namespace(eh.NamespaceFromContext(ctx))
-
-	r.dbMu.Lock()
-	defer r.dbMu.Unlock()
-	if _, ok := r.db[ns]; !ok {
-		r.db[ns] = map[uuid.UUID][]byte{}
-		r.ids[ns] = []uuid.UUID{}
-	}
-
-	return ns
-}
-
-// Repository returns a parent ReadRepo if there is one.
-func Repository(repo eh.ReadRepo) *Repo {
-	if repo == nil {
-		return nil
-	}
-
-	if r, ok := repo.(*Repo); ok {
-		return r
-	}
-
-	return Repository(repo.Parent())
-}
diff --git a/repo/mongodb/repo.go b/repo/mongodb/repo.go
index b5fefd4..cc62bbb 100644
--- a/repo/mongodb/repo.go
+++ b/repo/mongodb/repo.go
@@ -49,15 +49,13 @@ var (
 
 // Repo implements an MongoDB repository for entities.
 type Repo struct {
-	client     *mongo.Client
-	dbPrefix   string
-	collection string
-	factoryFn  func() eh.Entity
-	dbName     func(context.Context) string
+	client    *mongo.Client
+	entities  *mongo.Collection
+	newEntity func() eh.Entity
 }
 
 // NewRepo creates a new Repo.
-func NewRepo(uri, dbPrefix, collection string, options ...Option) (*Repo, error) {
+func NewRepo(uri, db, collection string, options ...Option) (*Repo, error) {
 	opts := mongoOptions.Client().ApplyURI(uri)
 	opts.SetWriteConcern(writeconcern.New(writeconcern.WMajority()))
 	opts.SetReadConcern(readconcern.Majority())
@@ -67,25 +65,18 @@ func NewRepo(uri, dbPrefix, collection string, options ...Option) (*Repo, error)
 		return nil, ErrCouldNotDialDB
 	}
 
-	return NewRepoWithClient(client, dbPrefix, collection, options...)
+	return NewRepoWithClient(client, db, collection, options...)
 }
 
 // NewRepoWithClient creates a new Repo with a client.
-func NewRepoWithClient(client *mongo.Client, dbPrefix, collection string, options ...Option) (*Repo, error) {
+func NewRepoWithClient(client *mongo.Client, db, collection string, options ...Option) (*Repo, error) {
 	if client == nil {
 		return nil, ErrNoDBClient
 	}
 
 	r := &Repo{
-		client:     client,
-		dbPrefix:   dbPrefix,
-		collection: collection,
-	}
-
-	// Use the a prefix and namespace from the context for DB name.
-	r.dbName = func(ctx context.Context) string {
-		ns := eh.NamespaceFromContext(ctx)
-		return dbPrefix + "_" + ns
+		client:   client,
+		entities: client.Database(db).Collection(collection),
 	}
 
 	for _, option := range options {
@@ -100,52 +91,41 @@ func NewRepoWithClient(client *mongo.Client, dbPrefix, collection string, option
 // Option is an option setter used to configure creation.
 type Option func(*Repo) error
 
-// WithPrefixAsDBName uses only the prefix as DB name, without namespace support.
-func WithPrefixAsDBName() Option {
-	return func(r *Repo) error {
-		r.dbName = func(context.Context) string {
-			return r.dbPrefix
-		}
-		return nil
-	}
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
+	return nil
 }
 
-// WithDBName uses a custom DB name function.
-func WithDBName(dbName func(context.Context) string) Option {
-	return func(r *Repo) error {
-		r.dbName = dbName
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
 		return nil
 	}
-}
-
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
-	return nil
+	if r, ok := repo.(*Repo); ok {
+		return r
+	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
 }
 
 // Find implements the Find method of the eventhorizon.ReadRepo interface.
 func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
-	if r.factoryFn == nil {
+	if r.newEntity == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	entity := r.factoryFn()
-	if err := c.FindOne(ctx, bson.M{"_id": id.String()}).Decode(entity); err == mongo.ErrNoDocuments {
+	entity := r.newEntity()
+	if err := r.entities.FindOne(ctx, bson.M{"_id": id.String()}).Decode(entity); err == mongo.ErrNoDocuments {
 		return nil, eh.RepoError{
-			Err:       eh.ErrEntityNotFound,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrEntityNotFound,
+			BaseErr: err,
 		}
 	} else if err != nil {
 		return nil, eh.RepoError{
-			Err:       eh.ErrCouldNotLoadEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotLoadEntity,
+			BaseErr: err,
 		}
 	}
 
@@ -154,32 +134,27 @@ func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 
 // FindAll implements the FindAll method of the eventhorizon.ReadRepo interface.
 func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
-	if r.factoryFn == nil {
+	if r.newEntity == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	cursor, err := c.Find(ctx, bson.M{})
+	cursor, err := r.entities.Find(ctx, bson.M{})
 	if err != nil {
 		return nil, eh.RepoError{
-			Err:       eh.ErrCouldNotLoadEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotLoadEntity,
+			BaseErr: err,
 		}
 	}
 
 	result := []eh.Entity{}
 	for cursor.Next(ctx) {
-		entity := r.factoryFn()
+		entity := r.newEntity()
 		if err := cursor.Decode(entity); err != nil {
 			return nil, eh.RepoError{
-				Err:       eh.ErrCouldNotLoadEntity,
-				BaseErr:   err,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err:     eh.ErrCouldNotLoadEntity,
+				BaseErr: err,
 			}
 		}
 		result = append(result, entity)
@@ -187,9 +162,8 @@ func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 
 	if err := cursor.Close(ctx); err != nil {
 		return nil, eh.RepoError{
-			Err:       eh.ErrCouldNotLoadEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotLoadEntity,
+			BaseErr: err,
 		}
 	}
 
@@ -200,7 +174,7 @@ func (r *Repo) FindAll(ctx context.Context) ([]eh.Entity, error) {
 type iter struct {
 	cursor    *mongo.Cursor
 	data      eh.Entity
-	factoryFn func() eh.Entity
+	newEntity func() eh.Entity
 	decodeErr error
 }
 
@@ -209,7 +183,7 @@ func (i *iter) Next(ctx context.Context) bool {
 		return false
 	}
 
-	item := i.factoryFn()
+	item := i.newEntity()
 	i.decodeErr = i.cursor.Decode(item)
 	i.data = item
 	return true
@@ -228,33 +202,28 @@ func (i *iter) Close(ctx context.Context) error {
 
 // FindCustomIter returns a mgo cursor you can use to stream results of very large datasets
 func (r *Repo) FindCustomIter(ctx context.Context, f func(context.Context, *mongo.Collection) (*mongo.Cursor, error)) (eh.Iter, error) {
-	if r.factoryFn == nil {
+	if r.newEntity == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	cursor, err := f(ctx, c)
+	cursor, err := f(ctx, r.entities)
 	if err != nil {
 		return nil, eh.RepoError{
-			Err:       ErrInvalidQuery,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     ErrInvalidQuery,
+			BaseErr: err,
 		}
 	}
 	if cursor == nil {
 		return nil, eh.RepoError{
-			Err:       ErrInvalidQuery,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrInvalidQuery,
 		}
 	}
 
 	return &iter{
 		cursor:    cursor,
-		factoryFn: r.factoryFn,
+		newEntity: r.newEntity,
 	}, nil
 }
 
@@ -264,48 +233,41 @@ func (r *Repo) FindCustomIter(ctx context.Context, f func(context.Context, *mong
 // the same query in FindCustom. Expect a ErrInvalidQuery if returning a nil
 // query from the callback.
 func (r *Repo) FindCustom(ctx context.Context, f func(context.Context, *mongo.Collection) (*mongo.Cursor, error)) ([]interface{}, error) {
-	if r.factoryFn == nil {
+	if r.newEntity == nil {
 		return nil, eh.RepoError{
-			Err:       ErrModelNotSet,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrModelNotSet,
 		}
 	}
 
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	cursor, err := f(ctx, c)
+	cursor, err := f(ctx, r.entities)
 	if err != nil {
 		return nil, eh.RepoError{
-			Err:       ErrInvalidQuery,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     ErrInvalidQuery,
+			BaseErr: err,
 		}
 	}
 	if cursor == nil {
 		return nil, eh.RepoError{
-			Err:       ErrInvalidQuery,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: ErrInvalidQuery,
 		}
 	}
 
 	result := []interface{}{}
-	entity := r.factoryFn()
+	entity := r.newEntity()
 	for cursor.Next(ctx) {
 		if err := cursor.Decode(entity); err != nil {
 			return nil, eh.RepoError{
-				Err:       eh.ErrCouldNotLoadEntity,
-				BaseErr:   err,
-				Namespace: eh.NamespaceFromContext(ctx),
+				Err:     eh.ErrCouldNotLoadEntity,
+				BaseErr: err,
 			}
 		}
 		result = append(result, entity)
-		entity = r.factoryFn()
+		entity = r.newEntity()
 	}
 	if err := cursor.Close(ctx); err != nil {
 		return nil, eh.RepoError{
-			Err:       eh.ErrCouldNotLoadEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotLoadEntity,
+			BaseErr: err,
 		}
 	}
 
@@ -316,15 +278,12 @@ func (r *Repo) FindCustom(ctx context.Context, f func(context.Context, *mongo.Co
 func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 	if entity.EntityID() == uuid.Nil {
 		return eh.RepoError{
-			Err:       eh.ErrCouldNotSaveEntity,
-			BaseErr:   eh.ErrMissingEntityID,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEntity,
+			BaseErr: eh.ErrMissingEntityID,
 		}
 	}
 
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	if _, err := c.UpdateOne(ctx,
+	if _, err := r.entities.UpdateOne(ctx,
 		bson.M{
 			"_id": entity.EntityID().String(),
 		},
@@ -334,9 +293,8 @@ func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 		options.Update().SetUpsert(true),
 	); err != nil {
 		return eh.RepoError{
-			Err:       eh.ErrCouldNotSaveEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotSaveEntity,
+			BaseErr: err,
 		}
 	}
 	return nil
@@ -344,18 +302,14 @@ func (r *Repo) Save(ctx context.Context, entity eh.Entity) error {
 
 // Remove implements the Remove method of the eventhorizon.WriteRepo interface.
 func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	if r, err := c.DeleteOne(ctx, bson.M{"_id": id.String()}); err != nil {
+	if r, err := r.entities.DeleteOne(ctx, bson.M{"_id": id.String()}); err != nil {
 		return eh.RepoError{
-			Err:       eh.ErrCouldNotRemoveEntity,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     eh.ErrCouldNotRemoveEntity,
+			BaseErr: err,
 		}
 	} else if r.DeletedCount == 0 {
 		return eh.RepoError{
-			Err:       eh.ErrEntityNotFound,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrEntityNotFound,
 		}
 	}
 
@@ -364,12 +318,9 @@ func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
 
 // Collection lets the function do custom actions on the collection.
 func (r *Repo) Collection(ctx context.Context, f func(context.Context, *mongo.Collection) error) error {
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	if err := f(ctx, c); err != nil {
+	if err := f(ctx, r.entities); err != nil {
 		return eh.RepoError{
-			Err:       err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: err,
 		}
 	}
 
@@ -378,18 +329,15 @@ func (r *Repo) Collection(ctx context.Context, f func(context.Context, *mongo.Co
 
 // SetEntityFactory sets a factory function that creates concrete entity types.
 func (r *Repo) SetEntityFactory(f func() eh.Entity) {
-	r.factoryFn = f
+	r.newEntity = f
 }
 
 // Clear clears the read model database.
 func (r *Repo) Clear(ctx context.Context) error {
-	c := r.client.Database(r.dbName(ctx)).Collection(r.collection)
-
-	if err := c.Drop(ctx); err != nil {
+	if err := r.entities.Drop(ctx); err != nil {
 		return eh.RepoError{
-			Err:       ErrCouldNotClearDB,
-			BaseErr:   err,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err:     ErrCouldNotClearDB,
+			BaseErr: err,
 		}
 	}
 	return nil
@@ -399,16 +347,3 @@ func (r *Repo) Clear(ctx context.Context) error {
 func (r *Repo) Close(ctx context.Context) {
 	r.client.Disconnect(ctx)
 }
-
-// Repository returns a parent ReadRepo if there is one.
-func Repository(repo eh.ReadRepo) *Repo {
-	if repo == nil {
-		return nil
-	}
-
-	if r, ok := repo.(*Repo); ok {
-		return r
-	}
-
-	return Repository(repo.Parent())
-}
diff --git a/repo/tracing/repo.go b/repo/tracing/repo.go
index 7cd3553..00a30b1 100644
--- a/repo/tracing/repo.go
+++ b/repo/tracing/repo.go
@@ -36,11 +36,23 @@ func NewRepo(repo eh.ReadWriteRepo) *Repo {
 	}
 }
 
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
 	return r.ReadWriteRepo
 }
 
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
+		return nil
+	}
+	if r, ok := repo.(*Repo); ok {
+		return r
+	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
+}
+
 // Find implements the Find method of the eventhorizon.ReadModel interface.
 func (r *Repo) Find(ctx context.Context, id uuid.UUID) (eh.Entity, error) {
 	sp, ctx := opentracing.StartSpanFromContext(ctx, "Repo.Find")
@@ -100,16 +112,3 @@ func (r *Repo) Remove(ctx context.Context, id uuid.UUID) error {
 
 	return err
 }
-
-// Repository returns a parent ReadRepo if there is one.
-func Repository(repo eh.ReadRepo) *Repo {
-	if repo == nil {
-		return nil
-	}
-
-	if r, ok := repo.(*Repo); ok {
-		return r
-	}
-
-	return Repository(repo.Parent())
-}
diff --git a/repo/version/context.go b/repo/version/context.go
index 86eccb2..e1ac4fe 100644
--- a/repo/version/context.go
+++ b/repo/version/context.go
@@ -46,7 +46,6 @@ func init() {
 
 type contextKey int
 
-// Context keys for namespace and min version.
 const (
 	minVersionKey contextKey = iota
 )
diff --git a/repo/version/repo.go b/repo/version/repo.go
index ae6246f..dbace72 100644
--- a/repo/version/repo.go
+++ b/repo/version/repo.go
@@ -36,11 +36,23 @@ func NewRepo(repo eh.ReadWriteRepo) *Repo {
 	}
 }
 
-// Parent implements the Parent method of the eventhorizon.ReadRepo interface.
-func (r *Repo) Parent() eh.ReadRepo {
+// InnerRepo implements the InnerRepo method of the eventhorizon.ReadRepo interface.
+func (r *Repo) InnerRepo(ctx context.Context) eh.ReadRepo {
 	return r.ReadWriteRepo
 }
 
+// IntoRepo tries to convert a eh.ReadRepo into a Repo by recursively looking at
+// inner repos. Returns nil if none was found.
+func IntoRepo(ctx context.Context, repo eh.ReadRepo) *Repo {
+	if repo == nil {
+		return nil
+	}
+	if r, ok := repo.(*Repo); ok {
+		return r
+	}
+	return IntoRepo(ctx, repo.InnerRepo(ctx))
+}
+
 // Find implements the Find method of the eventhorizon.ReadModel interface.
 // If the context contains a min version set by WithMinVersion it will only
 // return an item if its version is at least min version. If a timeout or
@@ -98,30 +110,15 @@ func (r *Repo) findMinVersion(ctx context.Context, id uuid.UUID, minVersion int)
 	versionable, ok := entity.(eh.Versionable)
 	if !ok {
 		return nil, eh.RepoError{
-			Err:       eh.ErrEntityHasNoVersion,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrEntityHasNoVersion,
 		}
 	}
 
 	if versionable.AggregateVersion() < minVersion {
 		return nil, eh.RepoError{
-			Err:       eh.ErrIncorrectEntityVersion,
-			Namespace: eh.NamespaceFromContext(ctx),
+			Err: eh.ErrIncorrectEntityVersion,
 		}
 	}
 
 	return entity, nil
 }
-
-// Repository returns a parent ReadRepo if there is one.
-func Repository(repo eh.ReadRepo) *Repo {
-	if repo == nil {
-		return nil
-	}
-
-	if r, ok := repo.(*Repo); ok {
-		return r
-	}
-
-	return Repository(repo.Parent())
-}


Test patch:
diff --git a/context_test.go b/context_test.go
index a104605..8b8f814 100644
--- a/context_test.go
+++ b/context_test.go
@@ -16,43 +16,9 @@ package eventhorizon
 
 import (
 	"context"
-	"encoding/json"
 	"testing"
 )
 
-func TestContextNamespace(t *testing.T) {
-	ctx := context.Background()
-
-	if ns := NamespaceFromContext(ctx); ns != DefaultNamespace {
-		t.Error("the namespace should be the default:", ns)
-	}
-
-	ctx = NewContextWithNamespace(ctx, "ns")
-	if ns := NamespaceFromContext(ctx); ns != "ns" {
-		t.Error("the namespace should be correct:", ns)
-	}
-
-	vals := MarshalContext(ctx)
-	if ns, ok := vals[namespaceKeyStr].(string); !ok || ns != "ns" {
-		t.Error("the marshaled namespace should be correct:", ns)
-	}
-	b, err := json.Marshal(vals)
-	if err != nil {
-		t.Error("could not marshal JSON:", err)
-	}
-
-	// Marshal via JSON to get more realistic testing.
-
-	vals = map[string]interface{}{}
-	if err := json.Unmarshal(b, &vals); err != nil {
-		t.Error("could not unmarshal JSON:", err)
-	}
-	ctx = UnmarshalContext(context.Background(), vals)
-	if ns := NamespaceFromContext(ctx); ns != "ns" {
-		t.Error("the namespace should be correct:", ns)
-	}
-}
-
 func TestContextMarshaler(t *testing.T) {
 	if len(contextMarshalFuncs) != 1 {
 		t.Error("there should be one context marshaler")
diff --git a/eventhandler/projector/eventhandler_test.go b/eventhandler/projector/eventhandler_test.go
index d2cb967..814c73b 100644
--- a/eventhandler/projector/eventhandler_test.go
+++ b/eventhandler/projector/eventhandler_test.go
@@ -257,7 +257,6 @@ func TestEventHandler_LoadError(t *testing.T) {
 	expectedErr := Error{
 		Err:          loadErr,
 		Projector:    TestProjectorType.String(),
-		Namespace:    eh.NamespaceFromContext(ctx),
 		EventVersion: 1,
 	}
 	err := handler.HandleEvent(ctx, event)
@@ -287,7 +286,6 @@ func TestEventHandler_SaveError(t *testing.T) {
 	expectedErr := Error{
 		Err:          saveErr,
 		Projector:    TestProjectorType.String(),
-		Namespace:    eh.NamespaceFromContext(ctx),
 		EventVersion: 1,
 	}
 	err := handler.HandleEvent(ctx, event)
@@ -317,7 +315,6 @@ func TestEventHandler_ProjectError(t *testing.T) {
 	expectedErr := Error{
 		Err:          projectErr,
 		Projector:    TestProjectorType.String(),
-		Namespace:    eh.NamespaceFromContext(ctx),
 		EventVersion: 1,
 	}
 	err := handler.HandleEvent(ctx, event)
diff --git a/eventstore/acceptanece_testing.go b/eventstore/acceptanece_testing.go
index 29c4bab..854f66f 100644
--- a/eventstore/acceptanece_testing.go
+++ b/eventstore/acceptanece_testing.go
@@ -31,12 +31,11 @@ import (
 // implementation:
 //
 //   func TestEventStore(t *testing.T) {
-//       ctx := context.Background() // Or other when testing namespaces.
 //       store := NewEventStore()
-//       eventstore.AcceptanceTest(t, ctx, store)
+//       eventstore.AcceptanceTest(t, store, context.Background())
 //   }
 //
-func AcceptanceTest(t *testing.T, ctx context.Context, store eh.EventStore) []eh.Event {
+func AcceptanceTest(t *testing.T, store eh.EventStore, ctx context.Context) []eh.Event {
 	savedEvents := []eh.Event{}
 
 	type contextKey string
diff --git a/eventstore/maintenance_testing.go b/eventstore/maintenance_testing.go
index 31269f4..a7434b2 100644
--- a/eventstore/maintenance_testing.go
+++ b/eventstore/maintenance_testing.go
@@ -29,12 +29,11 @@ import (
 // case in each implementation:
 //
 //   func TestEventStore(t *testing.T) {
-//       ctx := context.Background() // Or other when testing namespaces.
 //       store := NewEventStore()
-//       eventstore.AcceptanceTest(t, ctx, store)
+//       eventstore.AcceptanceTest(t, store, context.Background())
 //   }
 //
-func MaintenanceAcceptanceTest(t *testing.T, ctx context.Context, store eh.EventStore, storeMaintenance eh.EventStoreMaintenance) {
+func MaintenanceAcceptanceTest(t *testing.T, store eh.EventStore, storeMaintenance eh.EventStoreMaintenance, ctx context.Context) {
 	type contextKey string
 	ctx = context.WithValue(ctx, contextKey("testkey"), "testval")
 
diff --git a/eventstore/memory/eventmaintenance_test.go b/eventstore/memory/eventmaintenance_test.go
index 53ccbc9..46e9b6c 100644
--- a/eventstore/memory/eventmaintenance_test.go
+++ b/eventstore/memory/eventmaintenance_test.go
@@ -30,5 +30,5 @@ func TestEventStoreMaintenance(t *testing.T) {
 		t.Fatal("there should be a store")
 	}
 
-	eventstore.MaintenanceAcceptanceTest(t, context.Background(), store, store)
+	eventstore.MaintenanceAcceptanceTest(t, store, store, context.Background())
 }
diff --git a/eventstore/memory/eventstore_test.go b/eventstore/memory/eventstore_test.go
index 73e9c12..9be9cc5 100644
--- a/eventstore/memory/eventstore_test.go
+++ b/eventstore/memory/eventstore_test.go
@@ -34,10 +34,7 @@ func TestEventStore(t *testing.T) {
 		t.Fatal("there should be a store")
 	}
 
-	// Run the actual test suite, both for default and custom namespace.
-	eventstore.AcceptanceTest(t, context.Background(), store)
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	eventstore.AcceptanceTest(t, ctx, store)
+	eventstore.AcceptanceTest(t, store, context.Background())
 }
 
 func TestWithEventHandler(t *testing.T) {
diff --git a/eventstore/mongodb/eventmaintenance_test.go b/eventstore/mongodb/eventmaintenance_test.go
index 07c1700..2b0b14f 100644
--- a/eventstore/mongodb/eventmaintenance_test.go
+++ b/eventstore/mongodb/eventmaintenance_test.go
@@ -53,6 +53,5 @@ func TestEventStoreMaintenanceIntegration(t *testing.T) {
 	}
 	defer store.Close(context.Background())
 
-	// Additional tests for the maintenance, implemented by the store.
-	eventstore.MaintenanceAcceptanceTest(t, context.Background(), store, store)
+	eventstore.MaintenanceAcceptanceTest(t, store, store, context.Background())
 }
diff --git a/eventstore/mongodb/eventstore_test.go b/eventstore/mongodb/eventstore_test.go
index 99443af..3528809 100644
--- a/eventstore/mongodb/eventstore_test.go
+++ b/eventstore/mongodb/eventstore_test.go
@@ -57,11 +57,7 @@ func TestEventStoreIntegration(t *testing.T) {
 	}
 	defer store.Close(context.Background())
 
-	customNamespaceCtx := eh.NewContextWithNamespace(context.Background(), "ns")
-
-	// Run the actual test suite, both for default and custom namespace.
-	eventstore.AcceptanceTest(t, context.Background(), store)
-	eventstore.AcceptanceTest(t, customNamespaceCtx, store)
+	eventstore.AcceptanceTest(t, store, context.Background())
 }
 
 func TestWithEventHandlerIntegration(t *testing.T) {
diff --git a/eventstore/mongodb_v2/eventmaintenance_test.go b/eventstore/mongodb_v2/eventmaintenance_test.go
index 54fd13c..411efbe 100644
--- a/eventstore/mongodb_v2/eventmaintenance_test.go
+++ b/eventstore/mongodb_v2/eventmaintenance_test.go
@@ -53,6 +53,5 @@ func TestEventStoreMaintenanceIntegration(t *testing.T) {
 	}
 	defer store.Close(context.Background())
 
-	// Additional tests for the maintenance, implemented by the store.
-	eventstore.MaintenanceAcceptanceTest(t, context.Background(), store, store)
+	eventstore.MaintenanceAcceptanceTest(t, store, store, context.Background())
 }
diff --git a/eventstore/mongodb_v2/eventstore_test.go b/eventstore/mongodb_v2/eventstore_test.go
index 22166fd..abd6c29 100644
--- a/eventstore/mongodb_v2/eventstore_test.go
+++ b/eventstore/mongodb_v2/eventstore_test.go
@@ -58,8 +58,7 @@ func TestEventStoreIntegration(t *testing.T) {
 
 	defer store.Close(context.Background())
 
-	// Run the actual test suite.
-	eventstore.AcceptanceTest(t, context.Background(), store)
+	eventstore.AcceptanceTest(t, store, context.Background())
 }
 
 func TestWithEventHandlerIntegration(t *testing.T) {
diff --git a/eventstore/recorder/eventstore_test.go b/eventstore/recorder/eventstore_test.go
index 3a91254..10cc7b2 100644
--- a/eventstore/recorder/eventstore_test.go
+++ b/eventstore/recorder/eventstore_test.go
@@ -39,7 +39,7 @@ func TestEventStore(t *testing.T) {
 
 	// Run the actual test suite, with recording enabled.
 	store.StartRecording()
-	savedEvents := eventstore.AcceptanceTest(t, context.Background(), store)
+	savedEvents := eventstore.AcceptanceTest(t, store, context.Background())
 	store.StopRecording()
 
 	record := store.GetRecord()
diff --git a/eventstore/tracing/eventstore_test.go b/eventstore/tracing/eventstore_test.go
index 4afcc67..c3d2f3b 100644
--- a/eventstore/tracing/eventstore_test.go
+++ b/eventstore/tracing/eventstore_test.go
@@ -18,7 +18,6 @@ import (
 	"context"
 	"testing"
 
-	eh "github.com/looplab/eventhorizon"
 	"github.com/looplab/eventhorizon/eventstore"
 	"github.com/looplab/eventhorizon/eventstore/memory"
 )
@@ -38,8 +37,5 @@ func TestEventStore(t *testing.T) {
 		t.Fatal("there should be a store")
 	}
 
-	// Run the actual test suite, both for default and custom namespace.
-	eventstore.AcceptanceTest(t, context.Background(), store)
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	eventstore.AcceptanceTest(t, ctx, store)
+	eventstore.AcceptanceTest(t, store, context.Background())
 }
diff --git a/examples/guestlist/memory/memory_test.go b/examples/guestlist/memory/memory_test.go
index 75aab74..0182608 100644
--- a/examples/guestlist/memory/memory_test.go
+++ b/examples/guestlist/memory/memory_test.go
@@ -59,10 +59,8 @@ func Example() {
 	guestListRepo := memory.NewRepo()
 	guestListRepo.SetEntityFactory(func() eh.Entity { return &guestlist.GuestList{} })
 
-	// Set the namespace to use.
-	ctx, cancel := context.WithCancel(
-		eh.NewContextWithNamespace(context.Background(), "simple"),
-	)
+	// Create a context with cancellation.
+	ctx, cancel := context.WithCancel(context.Background())
 
 	// Setup a test utility waiter that waits for all 11 events to occur before
 	// evaluating results.
diff --git a/examples/guestlist/mongodb/mongodb_test.go b/examples/guestlist/mongodb/mongodb_test.go
index d766f1d..59473f5 100644
--- a/examples/guestlist/mongodb/mongodb_test.go
+++ b/examples/guestlist/mongodb/mongodb_test.go
@@ -88,10 +88,8 @@ guest list: 4 invited - 3 accepted, 1 declined - 2 confirmed, 1 denied`)
 	}
 	guestListRepo.SetEntityFactory(func() eh.Entity { return &guestlist.GuestList{} })
 
-	// Set the namespace to use.
-	ctx, cancel := context.WithCancel(
-		eh.NewContextWithNamespace(context.Background(), "mongodb"),
-	)
+	// Create a context with cancellation.
+	ctx, cancel := context.WithCancel(context.Background())
 
 	// Setup a test utility waiter that waits for all 11 events to occur before
 	// evaluating results.
diff --git a/examples/todomvc/backend/handler/handler_test.go b/examples/todomvc/backend/handler/handler_test.go
index f523c15..83acd78 100644
--- a/examples/todomvc/backend/handler/handler_test.go
+++ b/examples/todomvc/backend/handler/handler_test.go
@@ -84,8 +84,8 @@ func TestGetAll(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `[]` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.String() != `[]` {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	id := uuid.New()
@@ -115,8 +115,8 @@ func TestGetAll(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `[{"id":"`+id.String()+`","version":2,"items":[{"id":0,"desc":"desc","completed":false}],"created_at":"`+todo.TimeNow().Format(time.RFC3339Nano)+`","updated_at":"`+todo.TimeNow().Format(time.RFC3339Nano)+`"}]` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.String() != `[{"id":"`+id.String()+`","version":2,"items":[{"id":0,"desc":"desc","completed":false}],"created_at":"`+todo.TimeNow().Format(time.RFC3339Nano)+`","updated_at":"`+todo.TimeNow().Format(time.RFC3339Nano)+`"}]` {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	// Cancel all handlers and wait.
@@ -146,8 +146,8 @@ func TestCreate(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -212,8 +212,8 @@ func TestDelete(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -264,8 +264,8 @@ func TestAddItem(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -341,8 +341,8 @@ func TestRemoveItem(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -426,8 +426,8 @@ func TestRemoveCompleted(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -505,8 +505,8 @@ func TestSetItemDesc(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -588,8 +588,8 @@ func TestCheckItem(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -676,8 +676,8 @@ func TestCheckAllItems(t *testing.T) {
 	if w.Code != http.StatusOK {
 		t.Error("the status should be correct:", w.Code)
 	}
-	if string(w.Body.Bytes()) != `` {
-		t.Error("the body should be correct:", string(w.Body.Bytes()))
+	if w.Body.Len() != 0 {
+		t.Error("the body should be correct:", w.Body.String())
 	}
 
 	waiter := waiter.NewEventHandler()
@@ -791,11 +791,11 @@ func NewIntegrationTestSession(ctx context.Context) (
 	todoRepo := version.NewRepo(repo)
 
 	// NOTE: Temp clear of DB on startup.
-	mongoRepo, ok := todoRepo.Parent().(*mongodb.Repo)
-	if !ok {
+	mongodbRepo := mongodb.IntoRepo(ctx, todoRepo)
+	if mongodbRepo == nil {
 		log.Fatal("incorrect repo type")
 	}
-	if err := mongoRepo.Clear(ctx); err != nil {
+	if err := mongodbRepo.Clear(ctx); err != nil {
 		log.Println("could not clear DB:", err)
 	}
 
diff --git a/namespace/context_test.go b/namespace/context_test.go
new file mode 100644
index 0000000..cd9d030
--- /dev/null
+++ b/namespace/context_test.go
@@ -0,0 +1,56 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+	"encoding/json"
+	"testing"
+
+	eh "github.com/looplab/eventhorizon"
+)
+
+func TestContext(t *testing.T) {
+	ctx := context.Background()
+
+	if ns := FromContext(ctx); ns != DefaultNamespace {
+		t.Error("the namespace should be the default:", ns)
+	}
+
+	ctx = NewContext(ctx, "ns")
+	if ns := FromContext(ctx); ns != "ns" {
+		t.Error("the namespace should be correct:", ns)
+	}
+
+	vals := eh.MarshalContext(ctx)
+	if ns, ok := vals[namespaceKeyStr].(string); !ok || ns != "ns" {
+		t.Error("the marshaled namespace should be correct:", ns)
+	}
+	b, err := json.Marshal(vals)
+	if err != nil {
+		t.Error("could not marshal JSON:", err)
+	}
+
+	// Marshal via JSON to get more realistic testing.
+
+	vals = map[string]interface{}{}
+	if err := json.Unmarshal(b, &vals); err != nil {
+		t.Error("could not unmarshal JSON:", err)
+	}
+	ctx = eh.UnmarshalContext(context.Background(), vals)
+	if ns := FromContext(ctx); ns != "ns" {
+		t.Error("the namespace should be correct:", ns)
+	}
+}
diff --git a/namespace/eventstore_test.go b/namespace/eventstore_test.go
new file mode 100644
index 0000000..b732e5e
--- /dev/null
+++ b/namespace/eventstore_test.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+	"testing"
+
+	eh "github.com/looplab/eventhorizon"
+	"github.com/looplab/eventhorizon/eventstore"
+	"github.com/looplab/eventhorizon/eventstore/memory"
+)
+
+// NOTE: Not named "Integration" to enable running with the unit tests.
+func TestEventStore(t *testing.T) {
+	usedNamespaces := map[string]struct{}{}
+
+	store := NewEventStore(func(ns string) (eh.EventStore, error) {
+		usedNamespaces[ns] = struct{}{}
+		s, err := memory.NewEventStore()
+		if err != nil {
+			return nil, err
+		}
+		return s, nil
+	})
+	if store == nil {
+		t.Fatal("there should be a store")
+	}
+
+	// Default namespace.
+	eventstore.AcceptanceTest(t, store, context.Background())
+
+	// Other namespace.
+	ctx := NewContext(context.Background(), "other")
+	eventstore.AcceptanceTest(t, store, ctx)
+
+	if _, ok := usedNamespaces["default"]; !ok {
+		t.Error("the default namespace should have been used")
+	}
+	if _, ok := usedNamespaces["other"]; !ok {
+		t.Error("the other namespace should have been used")
+	}
+}
diff --git a/namespace/repo_test.go b/namespace/repo_test.go
new file mode 100644
index 0000000..f17dbe2
--- /dev/null
+++ b/namespace/repo_test.go
@@ -0,0 +1,69 @@
+// Copyright (c) 2021 - The Event Horizon authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package namespace
+
+import (
+	"context"
+	"testing"
+
+	eh "github.com/looplab/eventhorizon"
+	"github.com/looplab/eventhorizon/mocks"
+	"github.com/looplab/eventhorizon/repo"
+	"github.com/looplab/eventhorizon/repo/memory"
+)
+
+// NOTE: Not named "Integration" to enable running with the unit tests.
+func TestReadRepo(t *testing.T) {
+	repos := map[string]eh.ReadWriteRepo{}
+
+	r := NewRepo(func(ns string) (eh.ReadWriteRepo, error) {
+		r := memory.NewRepo()
+		r.SetEntityFactory(func() eh.Entity {
+			return &mocks.Model{}
+		})
+		repos[ns] = r
+		return r, nil
+	})
+	if r == nil {
+		t.Error("there should be a repository")
+	}
+
+	// Namespaces for testing; one default and one custom.
+	defaultCtx := context.Background()
+	otherCtx := NewContext(context.Background(), "other")
+
+	// Check that repos are created on access.
+	innerDefault := r.InnerRepo(defaultCtx)
+	defaultRepo, ok := repos["default"]
+	if !ok {
+		t.Error("the default namespace should have been used")
+	}
+	if innerDefault != defaultRepo {
+		t.Error("the default repo should be correct")
+	}
+
+	innerOther := r.InnerRepo(otherCtx)
+	otherRepo, ok := repos["other"]
+	if !ok {
+		t.Error("the other namespace should have been used")
+	}
+	if innerOther != otherRepo {
+		t.Error("the other repo should be correct")
+	}
+
+	// Test both namespaces.
+	repo.AcceptanceTest(t, r, defaultCtx)
+	repo.AcceptanceTest(t, r, otherCtx)
+}
diff --git a/repo/acceptance_testing.go b/repo/acceptance_testing.go
index 90d3066..d5f07c8 100644
--- a/repo/acceptance_testing.go
+++ b/repo/acceptance_testing.go
@@ -30,12 +30,11 @@ import (
 // implementation:
 //
 //   func TestRepo(t *testing.T) {
-//       ctx := context.Background() // Or other when testing namespaces.
 //       store := NewRepo()
-//       repo.AcceptanceTest(t, ctx, store)
+//       repo.AcceptanceTest(t, store, context.Background())
 //   }
 //
-func AcceptanceTest(t *testing.T, ctx context.Context, repo eh.ReadWriteRepo) {
+func AcceptanceTest(t *testing.T, repo eh.ReadWriteRepo, ctx context.Context) {
 	// Find non-existing item.
 	entity, err := repo.Find(ctx, uuid.New())
 	if rrErr, ok := err.(eh.RepoError); !ok || rrErr.Err != eh.ErrEntityNotFound {
diff --git a/repo/cache/repo_test.go b/repo/cache/repo_test.go
index b61768d..63746b1 100644
--- a/repo/cache/repo_test.go
+++ b/repo/cache/repo_test.go
@@ -35,32 +35,26 @@ func TestReadRepo(t *testing.T) {
 	if r == nil {
 		t.Error("there should be a repository")
 	}
-	if parent := r.Parent(); parent != baseRepo {
-		t.Error("the parent repo should be correct:", parent)
+	if inner := r.InnerRepo(context.Background()); inner != baseRepo {
+		t.Error("the inner repo should be correct:", inner)
 	}
 
-	// Read repository with default namespace.
-	repo.AcceptanceTest(t, context.Background(), r)
-
-	// Read repository with other namespace.
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	repo.AcceptanceTest(t, ctx, r)
-
+	repo.AcceptanceTest(t, r, context.Background())
 }
 
-func TestRepository(t *testing.T) {
-	if r := Repository(nil); r != nil {
-		t.Error("the parent repository should be nil:", r)
+func TestIntoRepo(t *testing.T) {
+	if r := IntoRepo(context.Background(), nil); r != nil {
+		t.Error("the repository should be nil:", r)
 	}
 
 	inner := &mocks.Repo{}
-	if r := Repository(inner); r != nil {
-		t.Error("the parent repository should be nil:", r)
+	if r := IntoRepo(context.Background(), inner); r != nil {
+		t.Error("the repository should be correct:", r)
 	}
 
-	r := NewRepo(inner)
-	outer := &mocks.Repo{ParentRepo: r}
-	if r := Repository(outer); r != r {
-		t.Error("the parent repository should be correct:", r)
+	middle := NewRepo(inner)
+	outer := &mocks.Repo{ParentRepo: middle}
+	if r := IntoRepo(context.Background(), outer); r != middle {
+		t.Error("the repository should be correct:", r)
 	}
 }
diff --git a/repo/memory/repo_test.go b/repo/memory/repo_test.go
index 4cac56a..8d9402a 100644
--- a/repo/memory/repo_test.go
+++ b/repo/memory/repo_test.go
@@ -33,32 +33,26 @@ func TestReadRepo(t *testing.T) {
 	r.SetEntityFactory(func() eh.Entity {
 		return &mocks.Model{}
 	})
-	if r.Parent() != nil {
-		t.Error("the parent repo should be nil")
+	if r.InnerRepo(context.Background()) != nil {
+		t.Error("the inner repo should be nil")
 	}
 
-	// Repo with default namespace.
-	repo.AcceptanceTest(t, context.Background(), r)
-
-	// Repo with other namespace
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	repo.AcceptanceTest(t, ctx, r)
-
+	repo.AcceptanceTest(t, r, context.Background())
 }
 
-func TestRepository(t *testing.T) {
-	if r := Repository(nil); r != nil {
-		t.Error("the parent repository should be nil:", r)
+func TestIntoRepo(t *testing.T) {
+	if r := IntoRepo(context.Background(), nil); r != nil {
+		t.Error("the repository should be nil:", r)
 	}
 
-	inner := &mocks.Repo{}
-	if r := Repository(inner); r != nil {
-		t.Error("the parent repository should be nil:", r)
+	other := &mocks.Repo{}
+	if r := IntoRepo(context.Background(), other); r != nil {
+		t.Error("the repository should be correct:", r)
 	}
 
-	repo := NewRepo()
-	outer := &mocks.Repo{ParentRepo: repo}
-	if r := Repository(outer); r != repo {
-		t.Error("the parent repository should be correct:", r)
+	inner := NewRepo()
+	outer := &mocks.Repo{ParentRepo: inner}
+	if r := IntoRepo(context.Background(), outer); r != inner {
+		t.Error("the repository should be correct:", r)
 	}
 }
diff --git a/repo/mongodb/repo_test.go b/repo/mongodb/repo_test.go
index 81db237..9d18dfe 100644
--- a/repo/mongodb/repo_test.go
+++ b/repo/mongodb/repo_test.go
@@ -65,19 +65,17 @@ func TestReadRepoIntegration(t *testing.T) {
 	r.SetEntityFactory(func() eh.Entity {
 		return &mocks.Model{}
 	})
-	if r.Parent() != nil {
-		t.Error("the parent repo should be nil")
+	if r.InnerRepo(context.Background()) != nil {
+		t.Error("the inner repo should be nil")
 	}
 
-	customNamespaceCtx := eh.NewContextWithNamespace(context.Background(), "ns")
-
-	repo.AcceptanceTest(t, context.Background(), r)
-	extraRepoTests(t, context.Background(), r)
-	repo.AcceptanceTest(t, customNamespaceCtx, r)
-	extraRepoTests(t, customNamespaceCtx, r)
+	repo.AcceptanceTest(t, r, context.Background())
+	extraRepoTests(t, r)
 }
 
-func extraRepoTests(t *testing.T, ctx context.Context, r *Repo) {
+func extraRepoTests(t *testing.T, r *Repo) {
+	ctx := context.Background()
+
 	// Insert a custom item.
 	modelCustom := &mocks.Model{
 		ID:        uuid.New(),
@@ -166,14 +164,14 @@ func extraRepoTests(t *testing.T, ctx context.Context, r *Repo) {
 	}
 }
 
-func TestRepository(t *testing.T) {
-	if r := Repository(nil); r != nil {
-		t.Error("the parent repository should be nil:", r)
+func TestIntoRepo(t *testing.T) {
+	if r := IntoRepo(context.Background(), nil); r != nil {
+		t.Error("the repository should be nil:", r)
 	}
 
-	inner := &mocks.Repo{}
-	if r := Repository(inner); r != nil {
-		t.Error("the parent repository should be nil:", r)
+	other := &mocks.Repo{}
+	if r := IntoRepo(context.Background(), other); r != nil {
+		t.Error("the repository should be correct:", r)
 	}
 
 	// Use MongoDB in Docker with fallback to localhost.
@@ -191,14 +189,14 @@ func TestRepository(t *testing.T) {
 	db := "test-" + hex.EncodeToString(b)
 	t.Log("using DB:", db)
 
-	repo, err := NewRepo(url, db, "mocks.Model")
+	inner, err := NewRepo(url, db, "mocks.Model")
 	if err != nil {
 		t.Error("there should be no error:", err)
 	}
-	defer repo.Close(context.Background())
+	defer inner.Close(context.Background())
 
-	outer := &mocks.Repo{ParentRepo: repo}
-	if r := Repository(outer); r != repo {
-		t.Error("the parent repository should be correct:", r)
+	outer := &mocks.Repo{ParentRepo: inner}
+	if r := IntoRepo(context.Background(), outer); r != inner {
+		t.Error("the repository should be correct:", r)
 	}
 }
diff --git a/repo/tracing/repo_test.go b/repo/tracing/repo_test.go
index 168c83c..96bb8f8 100644
--- a/repo/tracing/repo_test.go
+++ b/repo/tracing/repo_test.go
@@ -35,32 +35,26 @@ func TestReadRepo(t *testing.T) {
 	if r == nil {
 		t.Error("there should be a repository")
 	}
-	if parent := r.Parent(); parent != baseRepo {
-		t.Error("the parent repo should be correct:", parent)
+	if inner := r.InnerRepo(context.Background()); inner != baseRepo {
+		t.Error("the inner repo should be correct:", inner)
 	}
 
-	// Read repository with default namespace.
-	repo.AcceptanceTest(t, context.Background(), r)
-
-	// Read repository with other namespace.
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	repo.AcceptanceTest(t, ctx, r)
-
+	repo.AcceptanceTest(t, r, context.Background())
 }
 
-func TestRepository(t *testing.T) {
-	if r := Repository(nil); r != nil {
-		t.Error("the parent repository should be nil:", r)
+func TestIntoRepo(t *testing.T) {
+	if r := IntoRepo(context.Background(), nil); r != nil {
+		t.Error("the repository should be nil:", r)
 	}
 
 	inner := &mocks.Repo{}
-	if r := Repository(inner); r != nil {
-		t.Error("the parent repository should be nil:", r)
+	if r := IntoRepo(context.Background(), inner); r != nil {
+		t.Error("the repository should be correct:", r)
 	}
 
-	r := NewRepo(inner)
-	outer := &mocks.Repo{ParentRepo: r}
-	if r := Repository(outer); r != r {
-		t.Error("the parent repository should be correct:", r)
+	middle := NewRepo(inner)
+	outer := &mocks.Repo{ParentRepo: middle}
+	if r := IntoRepo(context.Background(), outer); r != middle {
+		t.Error("the repository should be correct:", r)
 	}
 }
diff --git a/repo/version/repo_test.go b/repo/version/repo_test.go
index 93a0b3a..3b3f527 100644
--- a/repo/version/repo_test.go
+++ b/repo/version/repo_test.go
@@ -39,22 +39,17 @@ func TestReadRepo(t *testing.T) {
 	if r == nil {
 		t.Error("there should be a repository")
 	}
-	if parent := r.Parent(); parent != baseRepo {
-		t.Error("the parent repo should be correct:", parent)
+	if inner := r.InnerRepo(context.Background()); inner != baseRepo {
+		t.Error("the inner repo should be correct:", inner)
 	}
 
-	// Read repository with default namespace.
-	repo.AcceptanceTest(t, context.Background(), r)
-	extraRepoTests(t, context.Background(), r, baseRepo)
-
-	// Read repository with other namespace.
-	ctx := eh.NewContextWithNamespace(context.Background(), "ns")
-	repo.AcceptanceTest(t, ctx, r)
-	extraRepoTests(t, ctx, r, baseRepo)
-
+	repo.AcceptanceTest(t, r, context.Background())
+	extraRepoTests(t, r, baseRepo)
 }
 
-func extraRepoTests(t *testing.T, ctx context.Context, r *Repo, baseRepo *memory.Repo) {
+func extraRepoTests(t *testing.T, r *Repo, baseRepo *memory.Repo) {
+	ctx := context.Background()
+
 	// Set a model without version for the first test case.
 	// A repo should not change models mid-life like this.
 	baseRepo.SetEntityFactory(func() eh.Entity {
@@ -255,19 +250,19 @@ func extraRepoTests(t *testing.T, ctx context.Context, r *Repo, baseRepo *memory
 	}
 }
 
-func TestRepository(t *testing.T) {
-	if r := Repository(nil); r != nil {
-		t.Error("the parent repository should be nil:", r)
+func TestIntoRepo(t *testing.T) {
+	if r := IntoRepo(context.Background(), nil); r != nil {
+		t.Error("the repository should be nil:", r)
 	}
 
 	inner := &mocks.Repo{}
-	if r := Repository(inner); r != nil {
-		t.Error("the parent repository should be nil:", r)
+	if r := IntoRepo(context.Background(), inner); r != nil {
+		t.Error("the repository should be correct:", r)
 	}
 
-	repo := NewRepo(inner)
-	outer := &mocks.Repo{ParentRepo: repo}
-	if r := Repository(outer); r != repo {
-		t.Error("the parent repository should be correct:", r)
+	middle := NewRepo(inner)
+	outer := &mocks.Repo{ParentRepo: middle}
+	if r := IntoRepo(context.Background(), outer); r != middle {
+		t.Error("the repository should be correct:", r)
 	}
 }