diff --git a/api/generated/openapi/zz_generated.openapi.go b/api/generated/openapi/zz_generated.openapi.go
index 44daf27f..eb9a6477 100644
--- a/api/generated/openapi/zz_generated.openapi.go
+++ b/api/generated/openapi/zz_generated.openapi.go
@@ -377,7 +377,7 @@ func schema_porch_api_porch_v1alpha1_FunctionEvalTaskSpec(ref common.ReferenceCa
 					},
 					"image": {
 						SchemaProps: spec.SchemaProps{
-							Description: "`Image` specifies the function image, such as `gcr.io/kpt-fn/gatekeeper:v0.2`. Use of `Image` is mutually exclusive with `FunctionRef`.",
+							Description: "`Image` specifies the function image, such as `gcr.io/kpt-fn/gatekeeper:v0.2`.",
 							Type:        []string{"string"},
 							Format:      "",
 						},
diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go
index 01ad29b9..96ed9dd4 100644
--- a/pkg/apiserver/apiserver.go
+++ b/pkg/apiserver/apiserver.go
@@ -27,8 +27,9 @@ import (
 	internalapi "github.com/nephio-project/porch/internal/api/porchinternal/v1alpha1"
 	"github.com/nephio-project/porch/internal/kpt/fnruntime"
 	"github.com/nephio-project/porch/pkg/cache"
-	memorycache "github.com/nephio-project/porch/pkg/cache/memory"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
 	"github.com/nephio-project/porch/pkg/engine"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
 	"github.com/nephio-project/porch/pkg/meta"
 	"github.com/nephio-project/porch/pkg/registry/porch"
 	"google.golang.org/api/option"
@@ -94,7 +95,7 @@ type Config struct {
 type PorchServer struct {
 	GenericAPIServer          *genericapiserver.GenericAPIServer
 	coreClient                client.WithWatch
-	cache                     cache.Cache
+	cache                     cachetypes.Cache
 	PeriodicRepoSyncFrequency time.Duration
 }
 
@@ -227,13 +228,22 @@ func (c completedConfig) New() (*PorchServer, error) {
 	userInfoProvider := &porch.ApiserverUserInfoProvider{}
 
 	watcherMgr := engine.NewWatcherManager()
-
-	memoryCache := memorycache.NewCache(c.ExtraConfig.CacheDirectory, c.ExtraConfig.RepoSyncFrequency, c.ExtraConfig.UseUserDefinedCaBundle, memorycache.CacheOptions{
-		CredentialResolver: credentialResolver,
-		UserInfoProvider:   userInfoProvider,
-		MetadataStore:      metadataStore,
-		ObjectNotifier:     watcherMgr,
-	})
+	cacheImpl, err := cache.CreateCacheImpl(
+		context.TODO(),
+		cachetypes.CacheOptions{
+			ExternalRepoOptions: externalrepotypes.ExternalRepoOptions{
+				LocalDirectory:         c.ExtraConfig.CacheDirectory,
+				UseUserDefinedCaBundle: c.ExtraConfig.UseUserDefinedCaBundle,
+				CredentialResolver:     credentialResolver,
+				UserInfoProvider:       userInfoProvider,
+			},
+			RepoSyncFrequency:    c.ExtraConfig.RepoSyncFrequency,
+			MetadataStore:        metadataStore,
+			RepoPRChangeNotifier: watcherMgr,
+		})
+	if err != nil {
+		return nil, fmt.Errorf("failed to creeate repository cache: %w", err)
+	}
 
 	runnerOptionsResolver := func(namespace string) fnruntime.RunnerOptions {
 		runnerOptions := fnruntime.RunnerOptions{}
@@ -243,7 +253,7 @@ func (c completedConfig) New() (*PorchServer, error) {
 	}
 
 	cad, err := engine.NewCaDEngine(
-		engine.WithCache(memoryCache),
+		engine.WithCache(cacheImpl),
 		// The order of registering the function runtimes matters here. When
 		// evaluating a function, the runtimes will be tried in the same
 		// order as they are registered.
@@ -268,7 +278,7 @@ func (c completedConfig) New() (*PorchServer, error) {
 	s := &PorchServer{
 		GenericAPIServer: genericServer,
 		coreClient:       coreClient,
-		cache:            memoryCache,
+		cache:            cacheImpl,
 		// Set background job periodic frequency the same as repo sync frequency.
 		PeriodicRepoSyncFrequency: c.ExtraConfig.RepoSyncFrequency,
 	}
diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go
index e68933fb..854bcdd2 100644
--- a/pkg/cache/cache.go
+++ b/pkg/cache/cache.go
@@ -17,11 +17,18 @@ package cache
 import (
 	"context"
 
-	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
-	"github.com/nephio-project/porch/pkg/repository"
+	memorycache "github.com/nephio-project/porch/pkg/cache/memorycache"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/trace"
 )
 
-type Cache interface {
-	OpenRepository(ctx context.Context, repositorySpec *configapi.Repository) (repository.Repository, error)
-	CloseRepository(ctx context.Context, repositorySpec *configapi.Repository, allRepos []configapi.Repository) error
+var tracer = otel.Tracer("cache")
+
+func CreateCacheImpl(ctx context.Context, options cachetypes.CacheOptions) (cachetypes.Cache, error) {
+	ctx, span := tracer.Start(ctx, "Repository::RepositoryFactory", trace.WithAttributes())
+	defer span.End()
+
+	var cacheFactory = new(memorycache.MemoryCacheFactory)
+	return cacheFactory.NewCache(ctx, options)
 }
diff --git a/pkg/cache/memory/cache.go b/pkg/cache/memory/cache.go
deleted file mode 100644
index 0389afe3..00000000
--- a/pkg/cache/memory/cache.go
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright 2022, 2024 The kpt and Nephio 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 memory
-
-import (
-	"context"
-	"errors"
-	"fmt"
-	"path/filepath"
-	"sync"
-	"time"
-
-	kptoci "github.com/GoogleContainerTools/kpt/pkg/oci"
-	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
-	"github.com/nephio-project/porch/pkg/cache"
-	"github.com/nephio-project/porch/pkg/git"
-	"github.com/nephio-project/porch/pkg/meta"
-	"github.com/nephio-project/porch/pkg/oci"
-	"github.com/nephio-project/porch/pkg/repository"
-	"go.opentelemetry.io/otel/trace"
-	"k8s.io/apimachinery/pkg/watch"
-)
-
-// Cache allows us to keep state for repositories, rather than querying them every time.
-//
-// Cache Structure:
-// <cacheDir>/git/
-// * Caches bare git repositories in directories named based on the repository address.
-// <cacheDir>/oci/
-// * Caches oci images with further hierarchy underneath
-// * We Cache image layers in <cacheDir>/oci/layers/ (this might be obsolete with the flattened Cache)
-// * We Cache flattened tar files in <cacheDir>/oci/ (so we don't need to pull to read resources)
-// * We poll the repositories (every minute) and Cache the discovered images in memory.
-type Cache struct {
-	mutex                  sync.Mutex
-	repositories           map[string]*cachedRepository
-	cacheDir               string
-	credentialResolver     repository.CredentialResolver
-	userInfoProvider       repository.UserInfoProvider
-	metadataStore          meta.MetadataStore
-	repoSyncFrequency      time.Duration
-	objectNotifier         objectNotifier
-	useUserDefinedCaBundle bool
-}
-
-var _ cache.Cache = &Cache{}
-
-type objectNotifier interface {
-	NotifyPackageRevisionChange(eventType watch.EventType, obj repository.PackageRevision) int
-}
-
-type CacheOptions struct {
-	CredentialResolver repository.CredentialResolver
-	UserInfoProvider   repository.UserInfoProvider
-	MetadataStore      meta.MetadataStore
-	ObjectNotifier     objectNotifier
-}
-
-func NewCache(cacheDir string, repoSyncFrequency time.Duration, useUserDefinedCaBundle bool, opts CacheOptions) *Cache {
-	return &Cache{
-		repositories:           make(map[string]*cachedRepository),
-		cacheDir:               cacheDir,
-		credentialResolver:     opts.CredentialResolver,
-		userInfoProvider:       opts.UserInfoProvider,
-		metadataStore:          opts.MetadataStore,
-		objectNotifier:         opts.ObjectNotifier,
-		repoSyncFrequency:      repoSyncFrequency,
-		useUserDefinedCaBundle: useUserDefinedCaBundle,
-	}
-}
-
-func getCacheKey(repositorySpec *configapi.Repository) (string, error) {
-	switch repositoryType := repositorySpec.Spec.Type; repositoryType {
-	case configapi.RepositoryTypeOCI:
-		ociSpec := repositorySpec.Spec.Oci
-		if ociSpec == nil {
-			return "", fmt.Errorf("oci not configured")
-		}
-		return "oci://" + ociSpec.Registry, nil
-
-	case configapi.RepositoryTypeGit:
-		gitSpec := repositorySpec.Spec.Git
-		if gitSpec == nil {
-			return "", errors.New("git property is required")
-		}
-		if gitSpec.Repo == "" {
-			return "", errors.New("git.repo property is required")
-		}
-		return fmt.Sprintf("git://%s/%s@%s/%s", gitSpec.Repo, gitSpec.Directory, repositorySpec.Namespace, repositorySpec.Name), nil
-
-	default:
-		return "", fmt.Errorf("repository type %q not supported", repositoryType)
-	}
-}
-
-func (c *Cache) OpenRepository(ctx context.Context, repositorySpec *configapi.Repository) (repository.Repository, error) {
-	ctx, span := tracer.Start(ctx, "Cache::OpenRepository", trace.WithAttributes())
-	defer span.End()
-
-	key, err := getCacheKey(repositorySpec)
-	if err != nil {
-		return nil, err
-	}
-	c.mutex.Lock()
-	defer c.mutex.Unlock()
-	cachedRepo := c.repositories[key]
-
-	switch repositoryType := repositorySpec.Spec.Type; repositoryType {
-	case configapi.RepositoryTypeOCI:
-		ociSpec := repositorySpec.Spec.Oci
-		if cachedRepo == nil {
-			cacheDir := filepath.Join(c.cacheDir, "oci")
-			storage, err := kptoci.NewStorage(cacheDir)
-			if err != nil {
-				return nil, err
-			}
-
-			r, err := oci.OpenRepository(repositorySpec.Name, repositorySpec.Namespace, ociSpec, repositorySpec.Spec.Deployment, storage)
-			if err != nil {
-				return nil, err
-			}
-			cachedRepo = newRepository(key, repositorySpec, r, c.objectNotifier, c.metadataStore, c.repoSyncFrequency)
-			c.repositories[key] = cachedRepo
-		}
-		return cachedRepo, nil
-
-	case configapi.RepositoryTypeGit:
-		gitSpec := repositorySpec.Spec.Git
-		if cachedRepo == nil {
-			var mbs git.MainBranchStrategy
-			if gitSpec.CreateBranch {
-				mbs = git.CreateIfMissing
-			} else {
-				mbs = git.ErrorIfMissing
-			}
-
-			r, err := git.OpenRepository(ctx, repositorySpec.Name, repositorySpec.Namespace, gitSpec, repositorySpec.Spec.Deployment, filepath.Join(c.cacheDir, "git"), git.GitRepositoryOptions{
-				CredentialResolver:     c.credentialResolver,
-				UserInfoProvider:       c.userInfoProvider,
-				MainBranchStrategy:     mbs,
-				UseUserDefinedCaBundle: c.useUserDefinedCaBundle,
-			})
-			if err != nil {
-				return nil, err
-			}
-
-			cachedRepo = newRepository(key, repositorySpec, r, c.objectNotifier, c.metadataStore, c.repoSyncFrequency)
-			c.repositories[key] = cachedRepo
-		} else {
-			// If there is an error from the background refresh goroutine, return it.
-			if err := cachedRepo.getRefreshError(); err != nil {
-				return nil, err
-			}
-		}
-		return cachedRepo, nil
-
-	default:
-		return nil, fmt.Errorf("type %q not supported", repositoryType)
-	}
-}
-
-func (c *Cache) CloseRepository(ctx context.Context, repositorySpec *configapi.Repository, allRepos []configapi.Repository) error {
-	_, span := tracer.Start(ctx, "Cache::CloseRepository", trace.WithAttributes())
-	defer span.End()
-
-	key, err := getCacheKey(repositorySpec)
-	if err != nil {
-		return err
-	}
-
-	// check if repositorySpec shares the underlying cached repo with another repository
-	for _, r := range allRepos {
-		if r.Name == repositorySpec.Name && r.Namespace == repositorySpec.Namespace {
-			continue
-		}
-		otherKey, err := getCacheKey(&r)
-		if err != nil {
-			return err
-		}
-		if otherKey == key {
-			// do not close cached repo if it is shared
-			return nil
-		}
-	}
-
-	var repository *cachedRepository
-	{
-		c.mutex.Lock()
-		if r, ok := c.repositories[key]; ok {
-			delete(c.repositories, key)
-			repository = r
-		}
-		c.mutex.Unlock()
-	}
-
-	if repository != nil {
-		return repository.Close()
-	} else {
-		return nil
-	}
-}
diff --git a/pkg/cache/memorycache/cache.go b/pkg/cache/memorycache/cache.go
new file mode 100644
index 00000000..aa94d1ef
--- /dev/null
+++ b/pkg/cache/memorycache/cache.go
@@ -0,0 +1,134 @@
+// Copyright 2022, 2024 The kpt and Nephio 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 memorycache
+
+import (
+	"context"
+	"errors"
+	"sync"
+
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
+	"github.com/nephio-project/porch/pkg/externalrepo"
+	"github.com/nephio-project/porch/pkg/repository"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/trace"
+)
+
+var tracer = otel.Tracer("memorycache")
+
+// Cache allows us to keep state for repositories, rather than querying them every time.
+//
+// Cache Structure:
+// <cacheDir>/git/
+// * Caches bare git repositories in directories named based on the repository address.
+// <cacheDir>/oci/
+// * Caches oci images with further hierarchy underneath
+// * We Cache image layers in <cacheDir>/oci/layers/ (this might be obsolete with the flattened Cache)
+// * We Cache flattened tar files in <cacheDir>/oci/ (so we don't need to pull to read resources)
+// * We poll the repositories periodically (configurable) and cache the discovered images in memory.
+type Cache struct {
+	mutex        sync.Mutex
+	repositories map[string]*cachedRepository
+	options      cachetypes.CacheOptions
+}
+
+var _ cachetypes.Cache = &Cache{}
+
+func (c *Cache) OpenRepository(ctx context.Context, repositorySpec *configapi.Repository) (repository.Repository, error) {
+	ctx, span := tracer.Start(ctx, "Cache::OpenRepository", trace.WithAttributes())
+	defer span.End()
+
+	key, err := externalrepo.RepositoryKey(repositorySpec)
+	if err != nil {
+		return nil, err
+	}
+
+	c.mutex.Lock()
+	defer c.mutex.Unlock()
+
+	if cachedRepo := c.repositories[key]; cachedRepo != nil {
+		// If there is an error from the background refresh goroutine, return it.
+		if err := cachedRepo.getRefreshError(); err == nil {
+			return cachedRepo, nil
+		} else {
+			return nil, err
+		}
+	}
+
+	externalRepo, err := externalrepo.CreateRepositoryImpl(ctx, repositorySpec, c.options.ExternalRepoOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	cachedRepo := newRepository(key, repositorySpec, externalRepo, c.options)
+	c.repositories[key] = cachedRepo
+
+	return cachedRepo, nil
+}
+
+func (c *Cache) UpdateRepository(ctx context.Context, repositorySpec *configapi.Repository) error {
+	return errors.New("update on memory cached repositories is not supported")
+}
+
+func (c *Cache) CloseRepository(ctx context.Context, repositorySpec *configapi.Repository, allRepos []configapi.Repository) error {
+	_, span := tracer.Start(ctx, "Cache::CloseRepository", trace.WithAttributes())
+	defer span.End()
+
+	key, err := externalrepo.RepositoryKey(repositorySpec)
+	if err != nil {
+		return err
+	}
+
+	// check if repositorySpec shares the underlying cached repo with another repository
+	for _, r := range allRepos {
+		if r.Name == repositorySpec.Name && r.Namespace == repositorySpec.Namespace {
+			continue
+		}
+		otherKey, err := externalrepo.RepositoryKey(&r)
+		if err != nil {
+			return err
+		}
+		if otherKey == key {
+			// do not close cached repo if it is shared
+			return nil
+		}
+	}
+
+	var repository *cachedRepository
+	{
+		c.mutex.Lock()
+		if r, ok := c.repositories[key]; ok {
+			delete(c.repositories, key)
+			repository = r
+		}
+		c.mutex.Unlock()
+	}
+
+	if repository != nil {
+		return repository.Close()
+	} else {
+		return nil
+	}
+}
+
+func (c *Cache) GetRepositories(ctx context.Context) []configapi.Repository {
+	repoSlice := []configapi.Repository{}
+
+	for _, repo := range c.repositories {
+		repoSlice = append(repoSlice, *repo.repoSpec)
+	}
+	return repoSlice
+}
diff --git a/pkg/cache/memory/cache_test.go b/pkg/cache/memorycache/cache_test.go
similarity index 90%
rename from pkg/cache/memory/cache_test.go
rename to pkg/cache/memorycache/cache_test.go
index 4f641509..1188a252 100644
--- a/pkg/cache/memory/cache_test.go
+++ b/pkg/cache/memorycache/cache_test.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package memory
+package memorycache
 
 import (
 	"context"
@@ -27,7 +27,9 @@ import (
 	"github.com/nephio-project/porch/api/porchconfig/v1alpha1"
 
 	fakecache "github.com/nephio-project/porch/pkg/cache/fake"
-	"github.com/nephio-project/porch/pkg/git"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
 	"github.com/nephio-project/porch/pkg/meta"
 	fakemeta "github.com/nephio-project/porch/pkg/meta/fake"
 	"github.com/nephio-project/porch/pkg/repository"
@@ -37,7 +39,7 @@ import (
 
 func TestLatestPackages(t *testing.T) {
 	ctx := context.Background()
-	testPath := filepath.Join("..", "..", "git", "testdata")
+	testPath := filepath.Join("..", "..", "externalrepo", "git", "testdata")
 
 	cachedRepo := openRepositoryFromArchive(t, ctx, testPath, "nested")
 
@@ -83,7 +85,7 @@ func TestLatestPackages(t *testing.T) {
 
 func TestPublishedLatest(t *testing.T) {
 	ctx := context.Background()
-	testPath := filepath.Join("..", "..", "git", "testdata")
+	testPath := filepath.Join("..", "..", "externalrepo", "git", "testdata")
 	cachedRepo := openRepositoryFromArchive(t, ctx, testPath, "nested")
 
 	revisions, err := cachedRepo.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{
@@ -129,7 +131,7 @@ func TestPublishedLatest(t *testing.T) {
 
 func TestDeletePublishedMain(t *testing.T) {
 	ctx := context.Background()
-	testPath := filepath.Join("../..", "git", "testdata")
+	testPath := filepath.Join("../..", "externalrepo", "git", "testdata")
 	cachedRepo := openRepositoryFromArchive(t, ctx, testPath, "nested")
 
 	revisions, err := cachedRepo.ListPackageRevisions(ctx, repository.ListPackageRevisionFilter{
@@ -223,10 +225,15 @@ func openRepositoryFromArchive(t *testing.T, ctx context.Context, testPath, name
 	_, address := git.ServeGitRepository(t, tarfile, tempdir)
 	metadataStore := createMetadataStoreFromArchive(t, fmt.Sprintf("%s-metadata.yaml", name), name)
 
-	cache := NewCache(t.TempDir(), 60*time.Second, true, CacheOptions{
-		MetadataStore:      metadataStore,
-		ObjectNotifier:     &fakecache.ObjectNotifier{},
-		CredentialResolver: &fakecache.CredentialResolver{},
+	cache, _ := new(MemoryCacheFactory).NewCache(ctx, cachetypes.CacheOptions{
+		ExternalRepoOptions: externalrepotypes.ExternalRepoOptions{
+			LocalDirectory:         t.TempDir(),
+			UseUserDefinedCaBundle: true,
+			CredentialResolver:     &fakecache.CredentialResolver{},
+		},
+		RepoSyncFrequency:    60 * time.Second,
+		MetadataStore:        metadataStore,
+		RepoPRChangeNotifier: &fakecache.ObjectNotifier{},
 	})
 	apiRepo := &v1alpha1.Repository{
 		TypeMeta: metav1.TypeMeta{
@@ -255,7 +262,7 @@ func openRepositoryFromArchive(t *testing.T, ctx context.Context, testPath, name
 		if err != nil {
 			t.Errorf("CloseRepository(%q) failed: %v", address, err)
 		}
-		if len(cache.repositories) != 0 {
+		if len(cache.GetRepositories(ctx)) != 0 {
 			t.Errorf("CloseRepository hasn't deleted repository from cache")
 		}
 	})
diff --git a/pkg/cache/memorycache/memorycachefactory.go b/pkg/cache/memorycache/memorycachefactory.go
new file mode 100644
index 00000000..7e45813d
--- /dev/null
+++ b/pkg/cache/memorycache/memorycachefactory.go
@@ -0,0 +1,33 @@
+// Copyright 2025 The kpt and Nephio 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 memorycache
+
+import (
+	"context"
+
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
+)
+
+var _ cachetypes.CacheFactory = &MemoryCacheFactory{}
+
+type MemoryCacheFactory struct {
+}
+
+func (f *MemoryCacheFactory) NewCache(_ context.Context, options cachetypes.CacheOptions) (cachetypes.Cache, error) {
+	return &Cache{
+		repositories: make(map[string]*cachedRepository),
+		options:      options,
+	}, nil
+}
diff --git a/pkg/cache/memory/package.go b/pkg/cache/memorycache/package.go
similarity index 98%
rename from pkg/cache/memory/package.go
rename to pkg/cache/memorycache/package.go
index d02093f2..929937da 100644
--- a/pkg/cache/memory/package.go
+++ b/pkg/cache/memorycache/package.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package memory
+package memorycache
 
 import (
 	"github.com/nephio-project/porch/pkg/repository"
diff --git a/pkg/cache/memory/packagerevision.go b/pkg/cache/memorycache/packagerevision.go
similarity index 99%
rename from pkg/cache/memory/packagerevision.go
rename to pkg/cache/memorycache/packagerevision.go
index 7ca2f5fa..631e49e4 100644
--- a/pkg/cache/memory/packagerevision.go
+++ b/pkg/cache/memorycache/packagerevision.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package memory
+package memorycache
 
 import (
 	"context"
diff --git a/pkg/cache/memory/repository.go b/pkg/cache/memorycache/repository.go
similarity index 91%
rename from pkg/cache/memory/repository.go
rename to pkg/cache/memorycache/repository.go
index 9363a61e..fbb1b3ed 100644
--- a/pkg/cache/memory/repository.go
+++ b/pkg/cache/memorycache/repository.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package memory
+package memorycache
 
 import (
 	"context"
@@ -22,10 +22,8 @@ import (
 
 	"github.com/nephio-project/porch/api/porch/v1alpha1"
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
-	"github.com/nephio-project/porch/pkg/git"
-	"github.com/nephio-project/porch/pkg/meta"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
 	"github.com/nephio-project/porch/pkg/repository"
-	"go.opentelemetry.io/otel"
 	"go.opentelemetry.io/otel/trace"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -34,8 +32,6 @@ import (
 	"k8s.io/klog/v2"
 )
 
-var tracer = otel.Tracer("cache")
-
 // We take advantage of the cache having a global view of all the packages
 // in a repository and compute the latest package revision in the cache
 // rather than add another level of caching in the repositories themselves.
@@ -61,25 +57,22 @@ type cachedRepository struct {
 	// This is returned back by the cache to the background goroutine when it calls periodicall to resync repositories.
 	refreshRevisionsError error
 
-	objectNotifier objectNotifier
-
-	metadataStore meta.MetadataStore
+	options cachetypes.CacheOptions
 }
 
-func newRepository(id string, repoSpec *configapi.Repository, repo repository.Repository, objectNotifier objectNotifier, metadataStore meta.MetadataStore, repoSyncFrequency time.Duration) *cachedRepository {
+func newRepository(id string, repoSpec *configapi.Repository, repo repository.Repository, options cachetypes.CacheOptions) *cachedRepository {
 	ctx, cancel := context.WithCancel(context.Background())
 	r := &cachedRepository{
-		id:             id,
-		repoSpec:       repoSpec,
-		repo:           repo,
-		cancel:         cancel,
-		objectNotifier: objectNotifier,
-		metadataStore:  metadataStore,
+		id:       id,
+		repoSpec: repoSpec,
+		repo:     repo,
+		cancel:   cancel,
+		options:  options,
 	}
 
 	// TODO: Should we fetch the packages here?
 
-	go r.pollForever(ctx, repoSyncFrequency)
+	go r.pollForever(ctx, options.RepoSyncFrequency)
 
 	return r
 }
@@ -153,12 +146,8 @@ func (r *cachedRepository) getCachedPackages(ctx context.Context, forceRefresh b
 		packages = nil
 		packageRevisions = nil
 
-		if gitRepo, isGitRepo := r.repo.(git.GitRepository); isGitRepo {
-			// TODO: Figure out a way to do this without the cache layer
-			//  needing to know what type of repo we are working with.
-			if err := gitRepo.UpdateDeletionProposedCache(); err != nil {
-				return nil, nil, err
-			}
+		if err := r.repo.Refresh(ctx); err != nil {
+			return nil, nil, err
 		}
 	}
 	r.mutex.Unlock()
@@ -292,13 +281,13 @@ func (r *cachedRepository) createMainPackageRevision(ctx context.Context, update
 	}
 
 	// Create the package if it doesn't exist
-	_, err := r.metadataStore.Get(ctx, pkgRevMetaNN)
+	_, err := r.options.MetadataStore.Get(ctx, pkgRevMetaNN)
 	if errors.IsNotFound(err) {
 		pkgRevMeta := metav1.ObjectMeta{
 			Name:      updatedMain.KubeObjectName(),
 			Namespace: updatedMain.KubeObjectNamespace(),
 		}
-		_, err := r.metadataStore.Create(ctx, pkgRevMeta, r.repoSpec.Name, updatedMain.UID())
+		_, err := r.options.MetadataStore.Create(ctx, pkgRevMeta, r.repoSpec.Name, updatedMain.UID())
 		if err != nil {
 			klog.Warningf("unable to create PackageRev CR for %s/%s: %v",
 				updatedMain.KubeObjectNamespace(), updatedMain.KubeObjectName(), err)
@@ -378,7 +367,7 @@ func (r *cachedRepository) Close() error {
 		// the repository, so we have to just delete the PackageRevision regardless of any
 		// finalizers.
 		klog.Infof("repo %s: deleting packagerev %s/%s because repository is closed", r.id, nn.Namespace, nn.Name)
-		_, err := r.metadataStore.Delete(context.TODO(), nn, true)
+		_, err := r.options.MetadataStore.Delete(context.TODO(), nn, true)
 		if err != nil {
 			// There isn't much use in returning an error here, so we just log it
 			// and create a PackageRevisionMeta with just name and namespace. This
@@ -386,7 +375,7 @@ func (r *cachedRepository) Close() error {
 			klog.Warningf("repo %s: error deleting packagerev for %s: %v", r.id, nn.Name, err)
 		}
 		klog.Infof("repo %s: successfully deleted packagerev %s/%s", r.id, nn.Namespace, nn.Name)
-		sent += r.objectNotifier.NotifyPackageRevisionChange(watch.Deleted, pr)
+		sent += r.options.RepoPRChangeNotifier.NotifyPackageRevisionChange(watch.Deleted, pr)
 	}
 	klog.Infof("repo %s: sent %d notifications for %d package revisions during close", r.id, sent, len(r.cachedPackageRevisions))
 	return r.repo.Close()
@@ -458,7 +447,7 @@ func (r *cachedRepository) refreshAllCachedPackages(ctx context.Context) (map[re
 	// Look up all existing PackageRevCRs so we an compare those to the
 	// actual Packagerevisions found in git/oci, and add/prune PackageRevCRs
 	// as necessary.
-	existingPkgRevCRs, err := r.metadataStore.List(ctx, r.repoSpec)
+	existingPkgRevCRs, err := r.options.MetadataStore.List(ctx, r.repoSpec)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -503,7 +492,7 @@ func (r *cachedRepository) refreshAllCachedPackages(ctx context.Context) (map[re
 		if _, found := newPackageRevisionNames[prm.Name]; !found {
 			klog.Infof("repo %s: deleting PackageRev %s/%s because parent PackageRevision was not found",
 				r.id, prm.Namespace, prm.Name)
-			if _, err := r.metadataStore.Delete(ctx, types.NamespacedName{
+			if _, err := r.options.MetadataStore.Delete(ctx, types.NamespacedName{
 				Name:      prm.Name,
 				Namespace: prm.Namespace,
 			}, true); err != nil {
@@ -522,10 +511,10 @@ func (r *cachedRepository) refreshAllCachedPackages(ctx context.Context) (map[re
 	for kname, newPackage := range newPackageRevisionNames {
 		oldPackage := oldPackageRevisionNames[kname]
 		if oldPackage == nil {
-			addSent += r.objectNotifier.NotifyPackageRevisionChange(watch.Added, newPackage)
+			addSent += r.options.RepoPRChangeNotifier.NotifyPackageRevisionChange(watch.Added, newPackage)
 		} else {
 			if oldPackage.ResourceVersion() != newPackage.ResourceVersion() {
-				modSent += r.objectNotifier.NotifyPackageRevisionChange(watch.Modified, newPackage)
+				modSent += r.options.RepoPRChangeNotifier.NotifyPackageRevisionChange(watch.Modified, newPackage)
 			}
 		}
 	}
@@ -538,7 +527,7 @@ func (r *cachedRepository) refreshAllCachedPackages(ctx context.Context) (map[re
 				Name:      pkgRevName,
 				Namespace: r.repoSpec.Namespace,
 			}
-			if _, err := r.metadataStore.Create(ctx, pkgRevMeta, r.repoSpec.Name, pkgRev.UID()); err != nil {
+			if _, err := r.options.MetadataStore.Create(ctx, pkgRevMeta, r.repoSpec.Name, pkgRev.UID()); err != nil {
 				// TODO: We should try to find a way to make these errors available through
 				// either the repository CR or the PackageRevision CR. This will be
 				// retried on the next sync.
@@ -558,7 +547,7 @@ func (r *cachedRepository) refreshAllCachedPackages(ctx context.Context) (map[re
 			}
 			klog.Infof("repo %s: deleting PackageRev %s/%s because PackageRevision was removed from SoT",
 				r.id, nn.Namespace, nn.Name)
-			delSent += r.objectNotifier.NotifyPackageRevisionChange(watch.Deleted, oldPackage)
+			delSent += r.options.RepoPRChangeNotifier.NotifyPackageRevisionChange(watch.Deleted, oldPackage)
 		}
 	}
 	klog.Infof("repo %s: addSent %d, modSent %d, delSent for %d old and %d new repo packages", r.id, addSent, modSent, len(oldPackageRevisionNames), len(newPackageRevisionNames))
diff --git a/pkg/cache/memory/util.go b/pkg/cache/memorycache/util.go
similarity index 99%
rename from pkg/cache/memory/util.go
rename to pkg/cache/memorycache/util.go
index 95203b69..d963f868 100644
--- a/pkg/cache/memory/util.go
+++ b/pkg/cache/memorycache/util.go
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package memory
+package memorycache
 
 import (
 	"context"
diff --git a/pkg/cache/types/cachetypes.go b/pkg/cache/types/cachetypes.go
new file mode 100644
index 00000000..e699ca85
--- /dev/null
+++ b/pkg/cache/types/cachetypes.go
@@ -0,0 +1,50 @@
+// Copyright 2025 The kpt and Nephio 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 cachetypes
+
+import (
+	"context"
+	"time"
+
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
+	"github.com/nephio-project/porch/pkg/meta"
+	"github.com/nephio-project/porch/pkg/repository"
+	"k8s.io/apimachinery/pkg/watch"
+)
+
+type CacheOptions struct {
+	ExternalRepoOptions  externalrepotypes.ExternalRepoOptions
+	RepoSyncFrequency    time.Duration
+	MetadataStore        meta.MetadataStore
+	RepoPRChangeNotifier RepoPRChangeNotifier
+	Driver               string
+	DataSource           string
+}
+
+type Cache interface {
+	OpenRepository(ctx context.Context, repositorySpec *configapi.Repository) (repository.Repository, error)
+	CloseRepository(ctx context.Context, repositorySpec *configapi.Repository, allRepos []configapi.Repository) error
+	GetRepositories(ctx context.Context) []configapi.Repository
+	UpdateRepository(ctx context.Context, repositorySpec *configapi.Repository) error
+}
+
+type CacheFactory interface {
+	NewCache(ctx context.Context, options CacheOptions) (Cache, error)
+}
+
+type RepoPRChangeNotifier interface {
+	NotifyPackageRevisionChange(eventType watch.EventType, obj repository.PackageRevision) int
+}
diff --git a/pkg/engine/engine.go b/pkg/engine/engine.go
index c80eb5e6..07e62d96 100644
--- a/pkg/engine/engine.go
+++ b/pkg/engine/engine.go
@@ -23,7 +23,7 @@ import (
 
 	api "github.com/nephio-project/porch/api/porch/v1alpha1"
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
-	cache "github.com/nephio-project/porch/pkg/cache"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
 	"github.com/nephio-project/porch/pkg/meta"
 	"github.com/nephio-project/porch/pkg/repository"
 	"github.com/nephio-project/porch/pkg/task"
@@ -73,7 +73,7 @@ func NewCaDEngine(opts ...EngineOption) (CaDEngine, error) {
 }
 
 type cadEngine struct {
-	cache cache.Cache
+	cache cachetypes.Cache
 
 	userInfoProvider repository.UserInfoProvider
 	metadataStore    meta.MetadataStore
diff --git a/pkg/engine/options.go b/pkg/engine/options.go
index 21388edb..d5406924 100644
--- a/pkg/engine/options.go
+++ b/pkg/engine/options.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 
 	"github.com/nephio-project/porch/internal/kpt/fnruntime"
-	"github.com/nephio-project/porch/pkg/cache"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
 	"github.com/nephio-project/porch/pkg/kpt"
 	"github.com/nephio-project/porch/pkg/kpt/fn"
 	"github.com/nephio-project/porch/pkg/meta"
@@ -38,7 +38,7 @@ func (f EngineOptionFunc) apply(engine *cadEngine) error {
 	return f(engine)
 }
 
-func WithCache(cache cache.Cache) EngineOption {
+func WithCache(cache cachetypes.Cache) EngineOption {
 	return EngineOptionFunc(func(engine *cadEngine) error {
 		engine.cache = cache
 		return nil
diff --git a/pkg/externalrepo/externalrepo.go b/pkg/externalrepo/externalrepo.go
new file mode 100644
index 00000000..2269f67e
--- /dev/null
+++ b/pkg/externalrepo/externalrepo.go
@@ -0,0 +1,75 @@
+// Copyright 2025 The kpt and Nephio 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 externalrepo
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
+	"github.com/nephio-project/porch/pkg/externalrepo/oci"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
+	"github.com/nephio-project/porch/pkg/repository"
+	"go.opentelemetry.io/otel"
+	"go.opentelemetry.io/otel/trace"
+)
+
+var tracer = otel.Tracer("externalrepo")
+
+func CreateRepositoryImpl(ctx context.Context, repositorySpec *configapi.Repository, options externalrepotypes.ExternalRepoOptions) (repository.Repository, error) {
+	ctx, span := tracer.Start(ctx, "Repository::RepositoryFactory", trace.WithAttributes())
+	defer span.End()
+
+	var repoFactory externalrepotypes.ExternalRepoFactory
+
+	switch repositoryType := repositorySpec.Spec.Type; repositoryType {
+	case configapi.RepositoryTypeOCI:
+		repoFactory = new(oci.OciRepoFactory)
+
+	case configapi.RepositoryTypeGit:
+		repoFactory = new(git.GitRepoFactory)
+
+	default:
+		return nil, fmt.Errorf("type %q not supported", repositoryType)
+	}
+
+	return repoFactory.NewRepositoryImpl(ctx, repositorySpec, options)
+}
+
+func RepositoryKey(repositorySpec *configapi.Repository) (string, error) {
+	switch repositoryType := repositorySpec.Spec.Type; repositoryType {
+	case configapi.RepositoryTypeOCI:
+		ociSpec := repositorySpec.Spec.Oci
+		if ociSpec == nil {
+			return "", fmt.Errorf("oci not configured")
+		}
+		return "oci://" + ociSpec.Registry, nil
+
+	case configapi.RepositoryTypeGit:
+		gitSpec := repositorySpec.Spec.Git
+		if gitSpec == nil {
+			return "", errors.New("git property is required")
+		}
+		if gitSpec.Repo == "" {
+			return "", errors.New("git.repo property is required")
+		}
+		return fmt.Sprintf("git://%s/%s@%s/%s", gitSpec.Repo, gitSpec.Directory, repositorySpec.Namespace, repositorySpec.Name), nil
+
+	default:
+		return "", fmt.Errorf("repository type %q not supported", repositoryType)
+	}
+}
diff --git a/pkg/repository/fake/packagerevision.go b/pkg/externalrepo/fake/packagerevision.go
similarity index 100%
rename from pkg/repository/fake/packagerevision.go
rename to pkg/externalrepo/fake/packagerevision.go
diff --git a/pkg/repository/fake/repository.go b/pkg/externalrepo/fake/repository.go
similarity index 98%
rename from pkg/repository/fake/repository.go
rename to pkg/externalrepo/fake/repository.go
index 864d5e89..37865564 100644
--- a/pkg/repository/fake/repository.go
+++ b/pkg/externalrepo/fake/repository.go
@@ -88,3 +88,7 @@ func (r *Repository) DeletePackage(_ context.Context, pr repository.Package) err
 func (r *Repository) Refresh(_ context.Context) error {
 	return nil
 }
+
+func (r *Repository) Key() string {
+	return ""
+}
diff --git a/pkg/git/annotation.go b/pkg/externalrepo/git/annotation.go
similarity index 100%
rename from pkg/git/annotation.go
rename to pkg/externalrepo/git/annotation.go
diff --git a/pkg/git/commit.go b/pkg/externalrepo/git/commit.go
similarity index 100%
rename from pkg/git/commit.go
rename to pkg/externalrepo/git/commit.go
diff --git a/pkg/git/commit_test.go b/pkg/externalrepo/git/commit_test.go
similarity index 100%
rename from pkg/git/commit_test.go
rename to pkg/externalrepo/git/commit_test.go
diff --git a/pkg/git/dir.go b/pkg/externalrepo/git/dir.go
similarity index 100%
rename from pkg/git/dir.go
rename to pkg/externalrepo/git/dir.go
diff --git a/pkg/git/dir_test.go b/pkg/externalrepo/git/dir_test.go
similarity index 100%
rename from pkg/git/dir_test.go
rename to pkg/externalrepo/git/dir_test.go
diff --git a/pkg/git/doc.go b/pkg/externalrepo/git/doc.go
similarity index 100%
rename from pkg/git/doc.go
rename to pkg/externalrepo/git/doc.go
diff --git a/pkg/git/draft.go b/pkg/externalrepo/git/draft.go
similarity index 100%
rename from pkg/git/draft.go
rename to pkg/externalrepo/git/draft.go
diff --git a/pkg/git/git.go b/pkg/externalrepo/git/git.go
similarity index 98%
rename from pkg/git/git.go
rename to pkg/externalrepo/git/git.go
index cacb3942..0868e5ad 100644
--- a/pkg/git/git.go
+++ b/pkg/externalrepo/git/git.go
@@ -38,6 +38,7 @@ import (
 	"github.com/go-git/go-git/v5/plumbing/transport"
 	"github.com/nephio-project/porch/api/porch/v1alpha1"
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
 	kptfilev1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1"
 	"github.com/nephio-project/porch/pkg/repository"
 	"go.opentelemetry.io/otel"
@@ -68,10 +69,8 @@ const (
 )
 
 type GitRepositoryOptions struct {
-	CredentialResolver     repository.CredentialResolver
-	UserInfoProvider       repository.UserInfoProvider
-	MainBranchStrategy     MainBranchStrategy
-	UseUserDefinedCaBundle bool
+	externalrepotypes.ExternalRepoOptions
+	MainBranchStrategy MainBranchStrategy
 }
 
 func OpenRepository(ctx context.Context, name, namespace string, spec *configapi.GitRepository, deployment bool, root string, opts GitRepositoryOptions) (GitRepository, error) {
@@ -136,14 +135,14 @@ func OpenRepository(ctx context.Context, name, namespace string, spec *configapi
 		branch:             branch,
 		directory:          strings.Trim(spec.Directory, "/"),
 		secret:             spec.SecretRef.Name,
-		credentialResolver: opts.CredentialResolver,
-		userInfoProvider:   opts.UserInfoProvider,
+		credentialResolver: opts.ExternalRepoOptions.CredentialResolver,
+		userInfoProvider:   opts.ExternalRepoOptions.UserInfoProvider,
 		cacheDir:           dir,
 		deployment:         deployment,
 	}
 
-	if opts.UseUserDefinedCaBundle {
-		if caBundle, err := opts.CredentialResolver.ResolveCredential(ctx, namespace, namespace+"-ca-bundle"); err != nil {
+	if opts.ExternalRepoOptions.UseUserDefinedCaBundle {
+		if caBundle, err := opts.ExternalRepoOptions.CredentialResolver.ResolveCredential(ctx, namespace, namespace+"-ca-bundle"); err != nil {
 			klog.Errorf("failed to obtain caBundle from secret %s/%s: %v", namespace, namespace+"-ca-bundle", err)
 		} else {
 			repository.caBundle = []byte(caBundle.ToString())
@@ -1708,7 +1707,11 @@ func (r *gitRepository) discoverPackagesInTree(commit *object.Commit, opt Discov
 }
 
 func (r *gitRepository) Refresh(_ context.Context) error {
-	return nil
+	return r.UpdateDeletionProposedCache()
+}
+
+func (r *gitRepository) Key() string {
+	return fmt.Sprintf("git://%s/%s@%s/%s", r.repo, r.directory, r.namespace, r.name)
 }
 
 // See https://eli.thegreenplace.net/2021/generic-functions-on-slices-with-go-type-parameters/
diff --git a/pkg/git/git_test.go b/pkg/externalrepo/git/git_test.go
similarity index 100%
rename from pkg/git/git_test.go
rename to pkg/externalrepo/git/git_test.go
diff --git a/pkg/externalrepo/git/gitrepofactory.go b/pkg/externalrepo/git/gitrepofactory.go
new file mode 100644
index 00000000..f1126830
--- /dev/null
+++ b/pkg/externalrepo/git/gitrepofactory.go
@@ -0,0 +1,56 @@
+// Copyright 2025 The kpt and Nephio 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 git
+
+import (
+	"context"
+	"errors"
+	"path/filepath"
+
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
+	"github.com/nephio-project/porch/pkg/repository"
+)
+
+var _ externalrepotypes.ExternalRepoFactory = &GitRepoFactory{}
+
+type GitRepoFactory struct {
+}
+
+func (f *GitRepoFactory) NewRepositoryImpl(ctx context.Context, repositorySpec *configapi.Repository, options externalrepotypes.ExternalRepoOptions) (repository.Repository, error) {
+	if repositorySpec.Spec.Git == nil {
+		return nil, errors.New("git property is required")
+	}
+	if repositorySpec.Spec.Git.Repo == "" {
+		return nil, errors.New("git.repo property is required")
+	}
+
+	var mbs MainBranchStrategy
+	if repositorySpec.Spec.Git.CreateBranch {
+		mbs = CreateIfMissing
+	} else {
+		mbs = ErrorIfMissing
+	}
+
+	repo, err := OpenRepository(ctx, repositorySpec.Name, repositorySpec.Namespace, repositorySpec.Spec.Git, repositorySpec.Spec.Deployment, filepath.Join(options.LocalDirectory, "git"), GitRepositoryOptions{
+		ExternalRepoOptions: options,
+		MainBranchStrategy:  mbs,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return repo, nil
+}
diff --git a/pkg/git/gogit.go b/pkg/externalrepo/git/gogit.go
similarity index 100%
rename from pkg/git/gogit.go
rename to pkg/externalrepo/git/gogit.go
diff --git a/pkg/git/mainbranchstrategy_string.go b/pkg/externalrepo/git/mainbranchstrategy_string.go
similarity index 100%
rename from pkg/git/mainbranchstrategy_string.go
rename to pkg/externalrepo/git/mainbranchstrategy_string.go
diff --git a/pkg/git/package.go b/pkg/externalrepo/git/package.go
similarity index 100%
rename from pkg/git/package.go
rename to pkg/externalrepo/git/package.go
diff --git a/pkg/git/package_test.go b/pkg/externalrepo/git/package_test.go
similarity index 100%
rename from pkg/git/package_test.go
rename to pkg/externalrepo/git/package_test.go
diff --git a/pkg/git/package_tree.go b/pkg/externalrepo/git/package_tree.go
similarity index 100%
rename from pkg/git/package_tree.go
rename to pkg/externalrepo/git/package_tree.go
diff --git a/pkg/git/primitives_test.go b/pkg/externalrepo/git/primitives_test.go
similarity index 100%
rename from pkg/git/primitives_test.go
rename to pkg/externalrepo/git/primitives_test.go
diff --git a/pkg/git/push.go b/pkg/externalrepo/git/push.go
similarity index 100%
rename from pkg/git/push.go
rename to pkg/externalrepo/git/push.go
diff --git a/pkg/git/ref.go b/pkg/externalrepo/git/ref.go
similarity index 100%
rename from pkg/git/ref.go
rename to pkg/externalrepo/git/ref.go
diff --git a/pkg/git/ref_test.go b/pkg/externalrepo/git/ref_test.go
similarity index 100%
rename from pkg/git/ref_test.go
rename to pkg/externalrepo/git/ref_test.go
diff --git a/pkg/git/repo.go b/pkg/externalrepo/git/repo.go
similarity index 100%
rename from pkg/git/repo.go
rename to pkg/externalrepo/git/repo.go
diff --git a/pkg/git/repos.go b/pkg/externalrepo/git/repos.go
similarity index 100%
rename from pkg/git/repos.go
rename to pkg/externalrepo/git/repos.go
diff --git a/pkg/git/testdata/.gitignore b/pkg/externalrepo/git/testdata/.gitignore
similarity index 100%
rename from pkg/git/testdata/.gitignore
rename to pkg/externalrepo/git/testdata/.gitignore
diff --git a/pkg/git/testdata/Makefile b/pkg/externalrepo/git/testdata/Makefile
similarity index 100%
rename from pkg/git/testdata/Makefile
rename to pkg/externalrepo/git/testdata/Makefile
diff --git a/pkg/git/testdata/README.md b/pkg/externalrepo/git/testdata/README.md
similarity index 100%
rename from pkg/git/testdata/README.md
rename to pkg/externalrepo/git/testdata/README.md
diff --git a/pkg/git/testdata/drafts-repository.md b/pkg/externalrepo/git/testdata/drafts-repository.md
similarity index 100%
rename from pkg/git/testdata/drafts-repository.md
rename to pkg/externalrepo/git/testdata/drafts-repository.md
diff --git a/pkg/git/testdata/drafts-repository.tar b/pkg/externalrepo/git/testdata/drafts-repository.tar
similarity index 100%
rename from pkg/git/testdata/drafts-repository.tar
rename to pkg/externalrepo/git/testdata/drafts-repository.tar
diff --git a/pkg/git/testdata/empty-repository.md b/pkg/externalrepo/git/testdata/empty-repository.md
similarity index 100%
rename from pkg/git/testdata/empty-repository.md
rename to pkg/externalrepo/git/testdata/empty-repository.md
diff --git a/pkg/git/testdata/empty-repository.tar b/pkg/externalrepo/git/testdata/empty-repository.tar
similarity index 100%
rename from pkg/git/testdata/empty-repository.tar
rename to pkg/externalrepo/git/testdata/empty-repository.tar
diff --git a/pkg/git/testdata/nested-repository.md b/pkg/externalrepo/git/testdata/nested-repository.md
similarity index 100%
rename from pkg/git/testdata/nested-repository.md
rename to pkg/externalrepo/git/testdata/nested-repository.md
diff --git a/pkg/git/testdata/nested-repository.tar b/pkg/externalrepo/git/testdata/nested-repository.tar
similarity index 100%
rename from pkg/git/testdata/nested-repository.tar
rename to pkg/externalrepo/git/testdata/nested-repository.tar
diff --git a/pkg/git/testdata/publishinfo-repository.md b/pkg/externalrepo/git/testdata/publishinfo-repository.md
similarity index 100%
rename from pkg/git/testdata/publishinfo-repository.md
rename to pkg/externalrepo/git/testdata/publishinfo-repository.md
diff --git a/pkg/git/testdata/publishinfo-repository.tar b/pkg/externalrepo/git/testdata/publishinfo-repository.tar
similarity index 100%
rename from pkg/git/testdata/publishinfo-repository.tar
rename to pkg/externalrepo/git/testdata/publishinfo-repository.tar
diff --git a/pkg/git/testdata/simple-repository.md b/pkg/externalrepo/git/testdata/simple-repository.md
similarity index 100%
rename from pkg/git/testdata/simple-repository.md
rename to pkg/externalrepo/git/testdata/simple-repository.md
diff --git a/pkg/git/testdata/simple-repository.tar b/pkg/externalrepo/git/testdata/simple-repository.tar
similarity index 100%
rename from pkg/git/testdata/simple-repository.tar
rename to pkg/externalrepo/git/testdata/simple-repository.tar
diff --git a/pkg/git/testdata/trivial-repository.md b/pkg/externalrepo/git/testdata/trivial-repository.md
similarity index 100%
rename from pkg/git/testdata/trivial-repository.md
rename to pkg/externalrepo/git/testdata/trivial-repository.md
diff --git a/pkg/git/testdata/trivial-repository.tar b/pkg/externalrepo/git/testdata/trivial-repository.tar
similarity index 100%
rename from pkg/git/testdata/trivial-repository.tar
rename to pkg/externalrepo/git/testdata/trivial-repository.tar
diff --git a/pkg/git/testing.go b/pkg/externalrepo/git/testing.go
similarity index 100%
rename from pkg/git/testing.go
rename to pkg/externalrepo/git/testing.go
diff --git a/pkg/git/testing_repo.go b/pkg/externalrepo/git/testing_repo.go
similarity index 100%
rename from pkg/git/testing_repo.go
rename to pkg/externalrepo/git/testing_repo.go
diff --git a/pkg/oci/doc.go b/pkg/externalrepo/oci/doc.go
similarity index 100%
rename from pkg/oci/doc.go
rename to pkg/externalrepo/oci/doc.go
diff --git a/pkg/oci/loader.go b/pkg/externalrepo/oci/loader.go
similarity index 100%
rename from pkg/oci/loader.go
rename to pkg/externalrepo/oci/loader.go
diff --git a/pkg/oci/mutate.go b/pkg/externalrepo/oci/mutate.go
similarity index 100%
rename from pkg/oci/mutate.go
rename to pkg/externalrepo/oci/mutate.go
diff --git a/pkg/oci/oci.go b/pkg/externalrepo/oci/oci.go
similarity index 97%
rename from pkg/oci/oci.go
rename to pkg/externalrepo/oci/oci.go
index e84427b3..331cc1d4 100644
--- a/pkg/oci/oci.go
+++ b/pkg/externalrepo/oci/oci.go
@@ -38,17 +38,6 @@ import (
 	"k8s.io/klog/v2"
 )
 
-func OpenRepository(name string, namespace string, spec *configapi.OciRepository, deployment bool, storage *oci.Storage) (repository.Repository, error) {
-	return &ociRepository{
-		name:       name,
-		namespace:  namespace,
-		spec:       *spec.DeepCopy(),
-		deployment: deployment,
-		storage:    storage,
-	}, nil
-
-}
-
 type ociRepository struct {
 	name       string
 	namespace  string
@@ -249,6 +238,10 @@ func (r *ociRepository) Refresh(_ context.Context) error {
 	return nil
 }
 
+func (r *ociRepository) Key() string {
+	return "oci://" + r.spec.Registry
+}
+
 // ToMainPackageRevision implements repository.PackageRevision.
 func (p *ociPackageRevision) ToMainPackageRevision() repository.PackageRevision {
 	panic("unimplemented")
diff --git a/pkg/externalrepo/oci/ocirepofactory.go b/pkg/externalrepo/oci/ocirepofactory.go
new file mode 100644
index 00000000..c63c1296
--- /dev/null
+++ b/pkg/externalrepo/oci/ocirepofactory.go
@@ -0,0 +1,52 @@
+// Copyright 2025 The kpt and Nephio 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 oci
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+
+	kptoci "github.com/GoogleContainerTools/kpt/pkg/oci"
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
+	"github.com/nephio-project/porch/pkg/repository"
+)
+
+var _ externalrepotypes.ExternalRepoFactory = &OciRepoFactory{}
+
+type OciRepoFactory struct {
+}
+
+func (f *OciRepoFactory) NewRepositoryImpl(ctx context.Context, repositorySpec *configapi.Repository, options externalrepotypes.ExternalRepoOptions) (repository.Repository, error) {
+	if repositorySpec.Spec.Oci == nil {
+		return nil, fmt.Errorf("oci not configured")
+	}
+
+	ociSpec := repositorySpec.Spec.Oci
+	localDir := filepath.Join(options.LocalDirectory, "oci")
+	storage, err := kptoci.NewStorage(localDir)
+	if err != nil {
+		return nil, err
+	}
+
+	return &ociRepository{
+		name:       repositorySpec.Name,
+		namespace:  repositorySpec.Namespace,
+		spec:       *ociSpec.DeepCopy(),
+		deployment: repositorySpec.Spec.Deployment,
+		storage:    storage,
+	}, nil
+}
diff --git a/pkg/externalrepo/types/externalrepotypes.go b/pkg/externalrepo/types/externalrepotypes.go
new file mode 100644
index 00000000..1d7fdaa6
--- /dev/null
+++ b/pkg/externalrepo/types/externalrepotypes.go
@@ -0,0 +1,33 @@
+// Copyright 2025 The kpt and Nephio 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 ExternalRepoied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package externalrepotypes
+
+import (
+	"context"
+
+	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	"github.com/nephio-project/porch/pkg/repository"
+)
+
+type ExternalRepoFactory interface {
+	NewRepositoryImpl(ctx context.Context, repositorySpec *configapi.Repository, options ExternalRepoOptions) (repository.Repository, error)
+}
+
+type ExternalRepoOptions struct {
+	LocalDirectory         string
+	UseUserDefinedCaBundle bool
+	CredentialResolver     repository.CredentialResolver
+	UserInfoProvider       repository.UserInfoProvider
+}
diff --git a/pkg/registry/porch/background.go b/pkg/registry/porch/background.go
index f739d386..389661e4 100644
--- a/pkg/registry/porch/background.go
+++ b/pkg/registry/porch/background.go
@@ -20,7 +20,7 @@ import (
 	"time"
 
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
-	"github.com/nephio-project/porch/pkg/cache"
+	cachetypes "github.com/nephio-project/porch/pkg/cache/types"
 	"k8s.io/apimachinery/pkg/api/meta"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/watch"
@@ -28,7 +28,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-func RunBackground(ctx context.Context, coreClient client.WithWatch, cache cache.Cache, PeriodicRepoSyncFrequency time.Duration) {
+func RunBackground(ctx context.Context, coreClient client.WithWatch, cache cachetypes.Cache, PeriodicRepoSyncFrequency time.Duration) {
 	b := background{
 		coreClient:                coreClient,
 		cache:                     cache,
@@ -40,7 +40,7 @@ func RunBackground(ctx context.Context, coreClient client.WithWatch, cache cache
 // background manages background tasks
 type background struct {
 	coreClient                client.WithWatch
-	cache                     cache.Cache
+	cache                     cachetypes.Cache
 	PeriodicRepoSyncFrequency time.Duration
 }
 
diff --git a/pkg/registry/porch/watch_test.go b/pkg/registry/porch/watch_test.go
index 7f1808ea..cfb3ed92 100644
--- a/pkg/registry/porch/watch_test.go
+++ b/pkg/registry/porch/watch_test.go
@@ -22,8 +22,8 @@ import (
 
 	"github.com/nephio-project/porch/api/porch/v1alpha1"
 	"github.com/nephio-project/porch/pkg/engine"
+	"github.com/nephio-project/porch/pkg/externalrepo/fake"
 	"github.com/nephio-project/porch/pkg/repository"
-	"github.com/nephio-project/porch/pkg/repository/fake"
 	metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
diff --git a/pkg/repository/repository.go b/pkg/repository/repository.go
index 90136a3d..7a8e4679 100644
--- a/pkg/repository/repository.go
+++ b/pkg/repository/repository.go
@@ -31,21 +31,62 @@ type PackageResources struct {
 }
 
 type PackageRevisionKey struct {
-	Repository, Package, Revision string
-	WorkspaceName                 v1alpha1.WorkspaceName
+	Namespace, Repository, Package, Revision string
+	WorkspaceName                            v1alpha1.WorkspaceName
 }
 
 func (n PackageRevisionKey) String() string {
-	return fmt.Sprintf("Repository: %q, Package: %q, Revision: %q, WorkspaceName: %q",
-		n.Repository, n.Package, n.Revision, string(n.WorkspaceName))
+	return fmt.Sprintf("%s.%s.%s.v%s.%s", n.Namespace, n.Repository, n.Package, n.Revision, string(n.WorkspaceName))
+}
+
+func (n PackageRevisionKey) NonNSString() string {
+	return fmt.Sprintf("%s.%s.v%s.%s", n.Repository, n.Package, n.Revision, string(n.WorkspaceName))
+}
+
+func (n PackageRevisionKey) PackageKey() PackageKey {
+	return PackageKey{
+		Namespace:  n.Namespace,
+		Repository: n.Repository,
+		Package:    n.Package,
+	}
+}
+
+func (n PackageRevisionKey) RepositoryKey() RepositoryKey {
+	return RepositoryKey{
+		Namespace:  n.Namespace,
+		Repository: n.Repository,
+	}
 }
 
 type PackageKey struct {
-	Repository, Package string
+	Namespace, Repository, Package string
 }
 
 func (n PackageKey) String() string {
-	return fmt.Sprintf("Repository: %q, Package: %q", n.Repository, n.Package)
+	return fmt.Sprintf("%s.%s.%s", n.Namespace, n.Repository, n.Package)
+}
+
+func (n PackageKey) NonNSString() string {
+	return fmt.Sprintf("%s.%s", n.Repository, n.Package)
+}
+
+func (n PackageKey) RepositoryKey() RepositoryKey {
+	return RepositoryKey{
+		Namespace:  n.Namespace,
+		Repository: n.Repository,
+	}
+}
+
+type RepositoryKey struct {
+	Namespace, Repository string
+}
+
+func (n RepositoryKey) String() string {
+	return fmt.Sprintf("%s.%s", n.Namespace, n.Repository)
+}
+
+func (n RepositoryKey) NonNSString() string {
+	return n.Repository
 }
 
 // PackageRevision is an abstract package version.
diff --git a/pkg/task/clone.go b/pkg/task/clone.go
index 4214d0f3..8ded29b7 100644
--- a/pkg/task/clone.go
+++ b/pkg/task/clone.go
@@ -23,7 +23,8 @@ import (
 	api "github.com/nephio-project/porch/api/porch/v1alpha1"
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
 	"github.com/nephio-project/porch/internal/kpt/builtins"
-	"github.com/nephio-project/porch/pkg/git"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
+	externalrepotypes "github.com/nephio-project/porch/pkg/externalrepo/types"
 	"github.com/nephio-project/porch/pkg/kpt"
 	v1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1"
 	"github.com/nephio-project/porch/pkg/repository"
@@ -155,7 +156,9 @@ func (m *clonePackageMutation) cloneFromGit(ctx context.Context, gitPackage *api
 	defer os.RemoveAll(dir)
 
 	r, err := git.OpenRepository(ctx, "", "", &spec, false, dir, git.GitRepositoryOptions{
-		CredentialResolver: m.credentialResolver,
+		ExternalRepoOptions: externalrepotypes.ExternalRepoOptions{
+			CredentialResolver: m.credentialResolver,
+		},
 		MainBranchStrategy: git.SkipVerification, // We are only reading so we don't need the main branch to exist.
 	})
 	if err != nil {
diff --git a/pkg/task/clone_test.go b/pkg/task/clone_test.go
index 6856d252..47da237e 100644
--- a/pkg/task/clone_test.go
+++ b/pkg/task/clone_test.go
@@ -36,7 +36,7 @@ import (
 	githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
 	"github.com/go-git/go-git/v5/storage/memory"
 	"github.com/nephio-project/porch/api/porch/v1alpha1"
-	"github.com/nephio-project/porch/pkg/git"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
 	"github.com/nephio-project/porch/pkg/repository"
 )
 
diff --git a/pkg/task/edit_test.go b/pkg/task/edit_test.go
index bedeb4fd..0dfb26ff 100644
--- a/pkg/task/edit_test.go
+++ b/pkg/task/edit_test.go
@@ -22,9 +22,9 @@ import (
 	"github.com/google/go-cmp/cmp"
 	"github.com/nephio-project/porch/api/porch/v1alpha1"
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
+	"github.com/nephio-project/porch/pkg/externalrepo/fake"
 	kptfile "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1"
 	"github.com/nephio-project/porch/pkg/repository"
-	"github.com/nephio-project/porch/pkg/repository/fake"
 )
 
 func TestEdit(t *testing.T) {
diff --git a/pkg/task/patch_test.go b/pkg/task/patch_test.go
index 32961e88..ceed10d6 100644
--- a/pkg/task/patch_test.go
+++ b/pkg/task/patch_test.go
@@ -21,9 +21,9 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	api "github.com/nephio-project/porch/api/porch/v1alpha1"
+	"github.com/nephio-project/porch/pkg/externalrepo/fake"
 	kptfile "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1"
 	"github.com/nephio-project/porch/pkg/repository"
-	"github.com/nephio-project/porch/pkg/repository/fake"
 )
 
 func TestSomething(t *testing.T) {
diff --git a/test/e2e/suite.go b/test/e2e/suite.go
index db6c8ffb..e61f8e72 100644
--- a/test/e2e/suite.go
+++ b/test/e2e/suite.go
@@ -37,7 +37,7 @@ import (
 	configapi "github.com/nephio-project/porch/api/porchconfig/v1alpha1"
 	internalapi "github.com/nephio-project/porch/internal/api/porchinternal/v1alpha1"
 	internalpkg "github.com/nephio-project/porch/internal/kpt/pkg"
-	"github.com/nephio-project/porch/pkg/git"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
 	kptfilev1 "github.com/nephio-project/porch/pkg/kpt/api/kptfile/v1"
 	"github.com/nephio-project/porch/pkg/repository"
 	appsv1 "k8s.io/api/apps/v1"
diff --git a/test/git/main.go b/test/git/main.go
index cd8f0699..0d684b3d 100644
--- a/test/git/main.go
+++ b/test/git/main.go
@@ -23,7 +23,7 @@ import (
 	"os"
 	"os/signal"
 
-	"github.com/nephio-project/porch/pkg/git"
+	"github.com/nephio-project/porch/pkg/externalrepo/git"
 	"k8s.io/klog/v2"
 )