diff --git a/api/kms/v1/routing.proto b/api/kms/v1/routing.proto
index 9de83a1c8ea6fa538ad5f834f2dfbfb465a8e7f9..36a21b948ccd84dc7cfb49bad00002e01eece6cd 100644
--- a/api/kms/v1/routing.proto
+++ b/api/kms/v1/routing.proto
@@ -4,7 +4,7 @@ package kms.v1;
 import "shared/v1/util.proto";
 
 // KmsRoutingService contains all calls that are used by the controller
-// to interact with a KMS so configure routes.
+// to interact with a KMS to configure routes.
 service KmsRoutingService {
   rpc AddRoute(AddRouteRequest) returns (AddRouteResponse);
   rpc GetRoute(GetRouteRequest) returns (GetRouteResponse);
diff --git a/ctrl/cmd/main.go b/ctrl/cmd/main.go
index 0f3a7905c8e6e5ae6301c5358ccb0033fcf50e81..2e9a2a80fc20ffc8720c9aaf288e613301d20924 100644
--- a/ctrl/cmd/main.go
+++ b/ctrl/cmd/main.go
@@ -26,7 +26,7 @@ func main() {
 	flag.StringVar(
 		&bindAddr,
 		"bindaddr",
-		"127.0.0.1:1337",
+		"127.0.0.1:1338",
 		"default address and port to bind the controller interface to",
 	)
 
diff --git a/docs/Tracing.md b/docs/Tracing.md
index 4f20d4313fe08a646a2fcf5dbfd53679600632d5..23cd612dee0a680ab5f344b8f3ec6f4ffb220e71 100644
--- a/docs/Tracing.md
+++ b/docs/Tracing.md
@@ -34,11 +34,10 @@ differentiate spans in a trace.
 
 ## Testing
 
-The KMS is instrumented with tracing support on its `HealthKms/Health` service.
+The KMS is instrumented with tracing support on its `kms.v1.HealthKmsService/Health` service.
 For now the traces are just emitted on stdout until we decided for a common
 trace backend.
 
 ```sh
-grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto localhost:1337 kms.v1.HealthKms/Health
-
+    grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto -d '{"message": "hello world"}' localhost:1337 kms.v1.HealthKmsService/Health
 ```
diff --git a/docs/kms.md b/docs/kms.md
new file mode 100644
index 0000000000000000000000000000000000000000..8ba13eb8b129eee01ac95e246c9149426a8bb610
--- /dev/null
+++ b/docs/kms.md
@@ -0,0 +1,19 @@
+# KMS
+
+## Manual interactions
+
+### Health
+
+```sh
+    grpcurl -plaintext -import-path api/ -proto api/kms/v1/health.proto localhost:1337 kms.v1.HealthKmsService/Health
+```
+
+### Routing
+
+```sh
+    grpcurl -plaintext -import-path api/ -proto api/kms/v1/routing.proto -d '{"route": {"id": "afeda267-edce-4ab6-af51-a14cf7009206", "next_hop_id": "626b3cbf-9de9-4d85-93da-185abc9ba904", "prev_hop_id": "a3a99140-41ef-4fdf-b1d1-70b947ab1491"}}' localhost:1337 kms.v1.KmsRoutingService/AddRoute
+```
+
+```sh
+    grpcurl -plaintext -import-path api/ -proto api/kms/v1/routing.proto -d '{"id": "afeda267-edce-4ab6-af51-a14cf7009206"}' localhost:1337 kms.v1.KmsRoutingService/GetRoute
+```
diff --git a/kms/cmd/main.go b/kms/cmd/main.go
index 4ce3a723e28399720d93abe656a611251387332f..dbae5a73f7b1b60690380e249296594da63a5a0d 100644
--- a/kms/cmd/main.go
+++ b/kms/cmd/main.go
@@ -33,7 +33,7 @@ func main() {
 	flag.StringVar(
 		&ctrlAddr,
 		"ctrladdr",
-		"127.0.0.1:1337",
+		"127.0.0.1:1338",
 		"address and port of the controller to manage this kms",
 	)
 
@@ -68,6 +68,7 @@ func main() {
 	go grpcServer.Start(
 		func(server *grpc.Server, app *application.Application) {
 			pb.RegisterHealthKmsServiceServer(server, app.HealthServer)
+			pb.RegisterKmsRoutingServiceServer(server, app.RoutingServer)
 		},
 	)
 
diff --git a/kms/internal/application/app.go b/kms/internal/application/app.go
index eac591d4b97973e7b2cdabba6bc50a2fc01567d5..debc80d3804d5d1cf1cd857f6cd4d8aecf266e07 100644
--- a/kms/internal/application/app.go
+++ b/kms/internal/application/app.go
@@ -1,7 +1,10 @@
 package application
 
 import (
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports"
 	"code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/interaction"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/store"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/service"
 	"code.fbi.h-da.de/danet/costaquanta/libs/logging"
 	sdktrace "go.opentelemetry.io/otel/sdk/trace"
 	"go.uber.org/zap"
@@ -10,6 +13,10 @@ import (
 type Application struct {
 	tracer       *sdktrace.TracerProvider
 	HealthServer *interaction.HealthServer
+
+	RoutingServer  *interaction.RoutingServer
+	RoutingService ports.RoutingService
+	RoutingStore   ports.RouteStore
 }
 
 func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Application {
@@ -17,8 +24,22 @@ func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Applicati
 	childTracer := tracer.Tracer("healthServer")
 	healthSrv := interaction.NewHealthServer(healthServerLogger, childTracer)
 
+	routingStoreLogger := logging.CreateChildLogger(log, "routingStore")
+	routingStoreTracer := tracer.Tracer("routingStore")
+	routingStore := store.NewInMemoryRouteStore(routingStoreLogger, routingStoreTracer)
+
+	routingServiceLogger := logging.CreateChildLogger(log, "routingService")
+	routingServiceTracer := tracer.Tracer("routingService")
+	rSrv := service.NewRoutingService(routingServiceLogger, routingServiceTracer, routingStore)
+
+	routingLogger := logging.CreateChildLogger(log, "routingServer")
+	routingTracer := tracer.Tracer("routingServer")
+	routerSrv := interaction.NewRoutingServer(routingLogger, routingTracer, rSrv)
+
 	return &Application{
-		HealthServer: healthSrv,
-		tracer:       tracer,
+		HealthServer:   healthSrv,
+		tracer:         tracer,
+		RoutingServer:  routerSrv,
+		RoutingService: rSrv,
 	}
 }
diff --git a/kms/internal/core/model/route.go b/kms/internal/core/model/route.go
new file mode 100644
index 0000000000000000000000000000000000000000..e8dea598f4afa99fc8cdce1941d6085ade2033ac
--- /dev/null
+++ b/kms/internal/core/model/route.go
@@ -0,0 +1,26 @@
+package model
+
+import "github.com/google/uuid"
+
+type Route struct {
+	// In other aspects used as route_id
+	ID uuid.UUID
+	// The ID of the KMS that a payload should be forwarded to if this route is used.
+	NextHopId string
+	// The ID of the KMS that a payload should be coming from if this route is used.
+	PrevHopId string
+	// The crypto algorithm that is used for this route.
+	// Is relevant for encrypting and decrypting a payload.
+	CryptoAlgorithm CryptoAlgorithm
+}
+
+// Describes the crypto algorithms that are possible.enum
+// More options can be added, this is a current selection useful for us.
+type CryptoAlgorithm int32
+
+const (
+	CryptoAlgorithm_CRYPTO_ALGORITHM_UNSPECIFIED CryptoAlgorithm = 0
+	CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM CryptoAlgorithm = 1
+	CryptoAlgorithm_CRYPTO_ALGORITHM_OTP         CryptoAlgorithm = 2
+	CryptoAlgorithm_CRYPTO_ALGORITHM_HYBRID      CryptoAlgorithm = 3
+)
diff --git a/kms/internal/core/ports/service.go b/kms/internal/core/ports/service.go
new file mode 100644
index 0000000000000000000000000000000000000000..5cd2fd4bcbf8fa9716a17b006a91f52c26070104
--- /dev/null
+++ b/kms/internal/core/ports/service.go
@@ -0,0 +1,15 @@
+package ports
+
+import (
+	"context"
+
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+)
+
+type RoutingService interface {
+	AddRoute(context.Context, model.Route) (model.Route, error)
+	DeleteRoute(context.Context, string) error
+	GetRoute(context.Context, string) (model.Route, error)
+	GetRouteList(context.Context) ([]model.Route, error)
+	UpdateRoute(context.Context, model.Route) error
+}
diff --git a/kms/internal/core/ports/store.go b/kms/internal/core/ports/store.go
new file mode 100644
index 0000000000000000000000000000000000000000..dc57456d7ea205a5f646fa024290ca0789b9f73c
--- /dev/null
+++ b/kms/internal/core/ports/store.go
@@ -0,0 +1,15 @@
+package ports
+
+import (
+	"context"
+
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+)
+
+type RouteStore interface {
+	AddRoute(context.Context, model.Route) (model.Route, error)
+	DeleteRoute(context.Context, string) error
+	GetRoute(context.Context, string) (model.Route, error)
+	GetRouteList(context.Context) ([]model.Route, error)
+	UpdateRoute(context.Context, model.Route) error
+}
diff --git a/kms/internal/infrastructure/interaction/routing.go b/kms/internal/infrastructure/interaction/routing.go
new file mode 100644
index 0000000000000000000000000000000000000000..8291fcc7d630a4ff471d23411646e1f1e1a5ee07
--- /dev/null
+++ b/kms/internal/infrastructure/interaction/routing.go
@@ -0,0 +1,164 @@
+package interaction
+
+import (
+	"context"
+
+	pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1"
+	sharedv1 "code.fbi.h-da.de/danet/costaquanta/gen/go/shared/v1"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports"
+	"github.com/google/uuid"
+	"go.opentelemetry.io/otel/trace"
+	"go.uber.org/zap"
+)
+
+type RoutingServer struct {
+	pb.UnimplementedKmsRoutingServiceServer
+
+	logger *zap.SugaredLogger
+	tracer trace.Tracer
+
+	srv ports.RoutingService
+}
+
+func NewRoutingServer(
+	logger *zap.SugaredLogger,
+	tracer trace.Tracer,
+	srv ports.RoutingService,
+) *RoutingServer {
+	s := &RoutingServer{
+		logger: logger,
+		tracer: tracer,
+		srv:    srv,
+	}
+
+	return s
+}
+
+func (r *RoutingServer) AddRoute(
+	ctx context.Context,
+	req *pb.AddRouteRequest,
+) (*pb.AddRouteResponse, error) {
+	_, span := r.tracer.Start(ctx, "add-route")
+	defer span.End()
+
+	uID, err := uuid.Parse(req.GetRoute().GetId())
+	if err != nil {
+		return nil, err
+	}
+
+	route := model.Route{
+		ID:              uID,
+		NextHopId:       req.GetRoute().GetNextHopId(),
+		PrevHopId:       req.GetRoute().GetPrevHopId(),
+		CryptoAlgorithm: model.CryptoAlgorithm(req.GetRoute().GetCryptoAlgorithm()),
+	}
+
+	createdRoute, err := r.srv.AddRoute(ctx, route)
+	if err != nil {
+		return &pb.AddRouteResponse{}, err
+	}
+
+	return &pb.AddRouteResponse{
+		Route: &pb.Route{
+			Id:              createdRoute.ID.String(),
+			NextHopId:       createdRoute.NextHopId,
+			PrevHopId:       createdRoute.PrevHopId,
+			CryptoAlgorithm: sharedv1.CryptoAlgorithm(createdRoute.CryptoAlgorithm),
+		},
+	}, nil
+}
+
+func (r *RoutingServer) DeleteRoute(
+	ctx context.Context,
+	req *pb.DeleteRouteRequest,
+) (*pb.DeleteRouteResponse, error) {
+	_, span := r.tracer.Start(ctx, "delete-route")
+	defer span.End()
+
+	err := r.srv.DeleteRoute(ctx, req.GetId())
+	if err != nil {
+		return nil, err
+	}
+
+	return &pb.DeleteRouteResponse{}, nil
+}
+
+func (r *RoutingServer) GetRoute(
+	ctx context.Context,
+	req *pb.GetRouteRequest,
+) (*pb.GetRouteResponse, error) {
+	_, span := r.tracer.Start(ctx, "get-route")
+	defer span.End()
+
+	route, err := r.srv.GetRoute(ctx, req.GetId())
+	if err != nil {
+		return nil, err
+	}
+
+	return &pb.GetRouteResponse{
+		Route: &pb.Route{
+			Id:              route.ID.String(),
+			NextHopId:       route.NextHopId,
+			PrevHopId:       route.PrevHopId,
+			CryptoAlgorithm: sharedv1.CryptoAlgorithm(route.CryptoAlgorithm),
+		},
+	}, nil
+}
+
+func (r *RoutingServer) GetRouteList(
+	ctx context.Context,
+	req *pb.GetRouteListRequest,
+) (*pb.GetRouteListResponse, error) {
+	_, span := r.tracer.Start(ctx, "get-route-list")
+	defer span.End()
+
+	r.logger.Debugf("got get route list %+v", req)
+
+	routes, err := r.srv.GetRouteList(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	pbRoutes := make([]*pb.Route, len(routes))
+
+	for i, route := range routes {
+		pbRoutes[i] = &pb.Route{
+			Id:              route.ID.String(),
+			NextHopId:       route.NextHopId,
+			PrevHopId:       route.PrevHopId,
+			CryptoAlgorithm: sharedv1.CryptoAlgorithm(route.CryptoAlgorithm),
+		}
+	}
+
+	return &pb.GetRouteListResponse{
+		Routes: pbRoutes,
+	}, nil
+}
+
+func (r *RoutingServer) UpdateRoute(
+	ctx context.Context,
+	req *pb.UpdateRouteRequest,
+) (*pb.UpdateRouteResponse, error) {
+	_, span := r.tracer.Start(ctx, "update-route")
+	defer span.End()
+
+	uID, err := uuid.Parse(req.GetRoute().GetId())
+	if err != nil {
+		return nil, err
+	}
+
+	route := model.Route{
+		ID:              uID,
+		NextHopId:       req.GetRoute().GetNextHopId(),
+		PrevHopId:       req.GetRoute().GetPrevHopId(),
+		CryptoAlgorithm: model.CryptoAlgorithm(req.GetRoute().GetCryptoAlgorithm()),
+	}
+
+	err = r.srv.UpdateRoute(ctx, route)
+	if err != nil {
+		return nil, err
+	}
+
+	return &pb.UpdateRouteResponse{}, nil
+}
diff --git a/kms/internal/infrastructure/interaction/routing_test.go b/kms/internal/infrastructure/interaction/routing_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ba61d83a1e5a3ed517890203ae3bd19398c38376
--- /dev/null
+++ b/kms/internal/infrastructure/interaction/routing_test.go
@@ -0,0 +1,464 @@
+package interaction
+
+import (
+	"context"
+	"log"
+	"net"
+	"testing"
+
+	pb "code.fbi.h-da.de/danet/costaquanta/gen/go/kms/v1"
+	pbs "code.fbi.h-da.de/danet/costaquanta/gen/go/shared/v1"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/infrastructure/store"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/service"
+	"code.fbi.h-da.de/danet/costaquanta/libs/tracing"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/zap"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/test/bufconn"
+)
+
+func newRoutingServer() (pb.KmsRoutingServiceServer, ports.RouteStore) {
+	logs := zap.NewExample()
+
+	traceProvider := tracing.GetTestTracer()
+	defer func() {
+		if err := traceProvider.Shutdown(context.Background()); err != nil {
+			logs.Info(err.Error())
+		}
+	}()
+
+	store := store.NewInMemoryRouteStore(logs.Sugar(), traceProvider.Tracer("test"))
+	svc := service.NewRoutingService(logs.Sugar(), traceProvider.Tracer("test"), store)
+	srv := NewRoutingServer(
+		zap.NewExample().Sugar(),
+		traceProvider.Tracer("test"),
+		svc,
+	)
+
+	return srv, store
+}
+
+func getTestRoutingServer() (pb.KmsRoutingServiceClient, func(), ports.RouteStore) {
+	buffer := 1024 * 1024
+	lis := bufconn.Listen(buffer)
+
+	baseServer := grpc.NewServer()
+	src, store := newRoutingServer()
+	pb.RegisterKmsRoutingServiceServer(baseServer, src)
+	go func() {
+		if err := baseServer.Serve(lis); err != nil {
+			log.Printf("error serving server: %v", err)
+		}
+	}()
+
+	conn, err := grpc.NewClient("passthrough:///bufnet",
+		grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
+			return lis.Dial()
+		}), grpc.WithTransportCredentials(insecure.NewCredentials()))
+	if err != nil {
+		log.Printf("error connecting to server: %v", err)
+	}
+
+	closer := func() {
+		err := lis.Close()
+		if err != nil {
+			log.Printf("error closing listener: %v", err)
+		}
+		baseServer.Stop()
+	}
+
+	client := pb.NewKmsRoutingServiceClient(conn)
+
+	return client, closer, store
+}
+
+func TestRoutingServer_AddRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Add new route", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+		testUUID := uuid.New()
+
+		client, closer, _ := getTestRoutingServer()
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		req := &pb.AddRouteRequest{
+			Route: &pb.Route{
+				Id:              testUUID.String(),
+				NextHopId:       "1337",
+				PrevHopId:       "1338",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		resp, err := client.AddRoute(ctx, req)
+		require.NoError(t, err)
+		compareRoutes(t, req.GetRoute(), resp.GetRoute())
+	})
+
+	t.Run("Add existing route should not fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+		testUUID := uuid.New()
+
+		client, closer, _ := getTestRoutingServer()
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		req := &pb.AddRouteRequest{
+			Route: &pb.Route{
+				Id:              testUUID.String(),
+				NextHopId:       "1337",
+				PrevHopId:       "1338",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		resp, err := client.AddRoute(ctx, req)
+		require.NoError(t, err)
+		compareRoutes(t, req.GetRoute(), resp.GetRoute())
+
+		resp, err = client.AddRoute(ctx, req)
+		require.Error(t, err)
+		assert.Empty(t, resp)
+	})
+
+	t.Run("Should fail if route ID is empty", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		req := &pb.AddRouteRequest{
+			Route: &pb.Route{
+				Id:              "",
+				NextHopId:       "1337",
+				PrevHopId:       "1338",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		resp, err := client.AddRoute(ctx, req)
+		require.Error(t, err)
+		assert.Empty(t, resp)
+	})
+}
+
+func TestRoutingServer_DeleteRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Delete an existing route should succeed", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+		testUUID := uuid.New()
+
+		client, closer, store := getTestRoutingServer()
+		routeInStore := model.Route{
+			ID:              testUUID,
+			NextHopId:       "1337",
+			PrevHopId:       "1338",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		_, err := store.AddRoute(ctx, routeInStore)
+		require.NoError(t, err)
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		deleteReq := &pb.DeleteRouteRequest{
+			Id: testUUID.String(),
+		}
+
+		deleteResp, err := client.DeleteRoute(ctx, deleteReq)
+		require.NoError(t, err)
+		assert.NotNil(t, deleteResp)
+
+		getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{
+			Id: testUUID.String(),
+		})
+		require.Error(t, err)
+		assert.Empty(t, getResp)
+	})
+
+	t.Run("Delete a non-existing route should fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		deleteReq := &pb.DeleteRouteRequest{
+			Id: uuid.New().String(),
+		}
+
+		deleteResp, err := client.DeleteRoute(ctx, deleteReq)
+		require.Error(t, err)
+		assert.Empty(t, deleteResp)
+	})
+}
+
+func TestRoutingServer_GetRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Get an existing route should succeed", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+		testUUID := uuid.New()
+
+		client, closer, store := getTestRoutingServer()
+
+		routeInStore := model.Route{
+			ID:              testUUID,
+			NextHopId:       "1337",
+			PrevHopId:       "1338",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		_, err := store.AddRoute(ctx, routeInStore)
+		require.NoError(t, err)
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{
+			Id: testUUID.String(),
+		})
+		require.NoError(t, err)
+		compareRouteToModel(t, routeInStore, getResp.GetRoute())
+	})
+
+	t.Run("Get a non-existing route should fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{
+			Id: uuid.New().String(),
+		})
+		require.Error(t, err)
+		assert.Empty(t, getResp)
+	})
+
+	t.Run("Get a route with an empty ID should fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{
+			Id: "",
+		})
+		require.Error(t, err)
+		assert.Empty(t, getResp)
+	})
+}
+
+func TestRoutingServer_GetRouteList(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Get an empty route list should succeed", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		getResp, err := client.GetRouteList(ctx, &pb.GetRouteListRequest{})
+		require.NoError(t, err)
+		assert.Empty(t, getResp.GetRoutes())
+	})
+
+	t.Run("Get a non-empty route list should succeed", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		testUUID1 := uuid.New()
+		testUUID2 := uuid.New()
+
+		client, closer, store := getTestRoutingServer()
+
+		routeInStore := model.Route{
+			ID:              testUUID1,
+			NextHopId:       "1337",
+			PrevHopId:       "1338",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		_, err := store.AddRoute(ctx, routeInStore)
+		require.NoError(t, err)
+
+		routeInStore2 := model.Route{
+			ID:              testUUID2,
+			NextHopId:       "1339",
+			PrevHopId:       "1340",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		_, err = store.AddRoute(ctx, routeInStore2)
+		require.NoError(t, err)
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		getResp, err := client.GetRouteList(ctx, &pb.GetRouteListRequest{})
+		require.NoError(t, err)
+
+		assert.Len(t, getResp.GetRoutes(), 2)
+
+		for _, route := range getResp.GetRoutes() {
+			if route.GetId() == testUUID1.String() {
+				compareRouteToModel(t, routeInStore, route)
+			} else if route.GetId() == testUUID2.String() {
+				compareRouteToModel(t, routeInStore2, route)
+			} else {
+				t.Errorf("Unexpected route ID: %s", route.GetId())
+			}
+		}
+	})
+}
+
+func TestRoutingServer_UpdateRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Update an existing route should succeed", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+		testUUID := uuid.New()
+
+		client, closer, store := getTestRoutingServer()
+
+		routeInStore := model.Route{
+			ID:              testUUID,
+			NextHopId:       "1337",
+			PrevHopId:       "1338",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		_, err := store.AddRoute(ctx, routeInStore)
+		require.NoError(t, err)
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		updateReq := &pb.UpdateRouteRequest{
+			Route: &pb.Route{
+				Id:              testUUID.String(),
+				NextHopId:       "1339",
+				PrevHopId:       "1340",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		updateResp, err := client.UpdateRoute(ctx, updateReq)
+		require.NoError(t, err)
+		assert.NotNil(t, updateResp)
+
+		getResp, err := client.GetRoute(ctx, &pb.GetRouteRequest{
+			Id: testUUID.String(),
+		})
+		require.NoError(t, err)
+		compareRoutes(t, updateReq.GetRoute(), getResp.GetRoute())
+	})
+
+	t.Run("Update a non-existing route should fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		updateReq := &pb.UpdateRouteRequest{
+			Route: &pb.Route{
+				Id:              uuid.New().String(),
+				NextHopId:       "1339",
+				PrevHopId:       "1340",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		updateResp, err := client.UpdateRoute(ctx, updateReq)
+		require.Error(t, err)
+		assert.Empty(t, updateResp)
+	})
+
+	t.Run("Update a route with an empty ID should fail", func(t *testing.T) {
+		t.Parallel()
+		ctx := context.Background()
+
+		client, closer, _ := getTestRoutingServer()
+
+		t.Cleanup(func() {
+			defer closer()
+		})
+
+		updateReq := &pb.UpdateRouteRequest{
+			Route: &pb.Route{
+				Id:              "",
+				NextHopId:       "1339",
+				PrevHopId:       "1340",
+				CryptoAlgorithm: pbs.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+			},
+		}
+
+		updateResp, err := client.UpdateRoute(ctx, updateReq)
+		require.Error(t, err)
+		assert.Empty(t, updateResp)
+	})
+}
+
+func compareRouteToModel(
+	t *testing.T,
+	expected model.Route,
+	actual *pb.Route,
+) {
+	t.Helper()
+
+	assert.Equal(t, expected.ID.String(), actual.GetId())
+	assert.Equal(t, expected.NextHopId, actual.GetNextHopId())
+	assert.Equal(t, expected.PrevHopId, actual.GetPrevHopId())
+	assert.Equal(t, expected.CryptoAlgorithm, model.CryptoAlgorithm(actual.GetCryptoAlgorithm()))
+}
+
+func compareRoutes(
+	t *testing.T,
+	expected *pb.Route,
+	actual *pb.Route,
+) {
+	t.Helper()
+
+	assert.Equal(t, expected.GetId(), actual.GetId())
+	assert.Equal(t, expected.GetNextHopId(), actual.GetNextHopId())
+	assert.Equal(t, expected.GetPrevHopId(), actual.GetPrevHopId())
+	assert.Equal(t, expected.GetCryptoAlgorithm(), actual.GetCryptoAlgorithm())
+}
diff --git a/kms/internal/infrastructure/store/route.go b/kms/internal/infrastructure/store/route.go
new file mode 100644
index 0000000000000000000000000000000000000000..a93b169cc25c662310042448ee34c8c26d7d866c
--- /dev/null
+++ b/kms/internal/infrastructure/store/route.go
@@ -0,0 +1,168 @@
+package store
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+	"github.com/google/uuid"
+	"go.opentelemetry.io/otel/trace"
+	"go.uber.org/zap"
+)
+
+type RouteStore interface {
+	AddRoute(context.Context, model.Route) (model.Route, error)
+	DeleteRoute(context.Context, string) error
+	GetRoute(context.Context, string) (model.Route, error)
+	GetRouteList(context.Context) ([]model.Route, error)
+	UpdateRoute(context.Context, model.Route) error
+}
+
+type InMemoryRouteStore struct {
+	dataStore map[uuid.UUID]model.Route
+
+	// Using a RWMutex to allow multiple readers concurrently.
+	// A write lock can only be acquired once all readers are done and release their locks.
+	mu sync.RWMutex
+
+	logger *zap.SugaredLogger
+	tracer trace.Tracer
+}
+
+func NewInMemoryRouteStore(logger *zap.SugaredLogger, tracer trace.Tracer) *InMemoryRouteStore {
+	return &InMemoryRouteStore{
+		dataStore: make(map[uuid.UUID]model.Route),
+		mu:        sync.RWMutex{},
+		logger:    logger,
+		tracer:    tracer,
+	}
+}
+
+func (s *InMemoryRouteStore) AddRoute(ctx context.Context, route model.Route) (model.Route, error) {
+	_, span := s.tracer.Start(ctx, "add-route")
+	defer span.End()
+
+	s.logger.Debugw("Adding route to store",
+		"routeId", route.ID,
+		"nextHopId", route.NextHopId,
+		"prevHopId", route.PrevHopId,
+		"cryptoAlgorithm", route.CryptoAlgorithm)
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if _, exists := s.dataStore[route.ID]; exists {
+		s.logger.Errorw("Route already exists in store",
+			"routeId", route.ID.String())
+
+		return model.Route{}, fmt.Errorf("route %s already exists in store", route.ID)
+	}
+
+	s.dataStore[route.ID] = route
+
+	return route, nil
+}
+
+func (s *InMemoryRouteStore) DeleteRoute(ctx context.Context, routeID string) error {
+	_, span := s.tracer.Start(ctx, "delete-route")
+	defer span.End()
+
+	s.logger.Debugw("Deleting route from store",
+		"routeId", routeID)
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	id, err := uuid.Parse(routeID)
+	if err != nil {
+		s.logger.Errorw("Failed to parse route ID",
+			"routeId", routeID,
+			"error", err)
+
+		return fmt.Errorf("failed to parse route ID: %w", err)
+	}
+
+	if _, exists := s.dataStore[id]; !exists {
+		s.logger.Errorw("Route does not exist in store",
+			"routeId", id.String())
+
+		return fmt.Errorf("route %s does not exist in store", id)
+	}
+
+	delete(s.dataStore, id)
+
+	return nil
+}
+
+func (s *InMemoryRouteStore) GetRoute(ctx context.Context, routeID string) (model.Route, error) {
+	_, span := s.tracer.Start(ctx, "get-route")
+	defer span.End()
+
+	s.logger.Debugw("Getting route from store",
+		"routeId", routeID)
+
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	id, err := uuid.Parse(routeID)
+	if err != nil {
+		s.logger.Errorw("Failed to parse route ID",
+			"routeId", routeID,
+			"error", err)
+
+		return model.Route{}, fmt.Errorf("failed to parse route ID: %w", err)
+	}
+
+	route, exists := s.dataStore[id]
+	if !exists {
+		s.logger.Errorw("Route does not exist in store",
+			"routeId", id.String())
+
+		return model.Route{}, fmt.Errorf("route %s does not exist in store", id)
+	}
+
+	return route, nil
+}
+
+func (s *InMemoryRouteStore) GetRouteList(ctx context.Context) ([]model.Route, error) {
+	_, span := s.tracer.Start(ctx, "get-route-list")
+	defer span.End()
+
+	s.logger.Debug("Getting route list from store")
+
+	s.mu.RLock()
+	defer s.mu.RUnlock()
+
+	routes := make([]model.Route, 0, len(s.dataStore))
+	for _, route := range s.dataStore {
+		routes = append(routes, route)
+	}
+
+	return routes, nil
+}
+
+func (s *InMemoryRouteStore) UpdateRoute(ctx context.Context, route model.Route) error {
+	_, span := s.tracer.Start(ctx, "update-route")
+	defer span.End()
+
+	s.logger.Debugw("Updating route in store",
+		"routeId", route.ID,
+		"nextHopId", route.NextHopId,
+		"prevHopId", route.PrevHopId,
+		"cryptoAlgorithm", route.CryptoAlgorithm)
+
+	s.mu.Lock()
+	defer s.mu.Unlock()
+
+	if _, exists := s.dataStore[route.ID]; !exists {
+		s.logger.Errorw("Route does not exist in store",
+			"routeId", route.ID.String())
+
+		return fmt.Errorf("route %s does not exist in store", route.ID)
+	}
+
+	s.dataStore[route.ID] = route
+
+	return nil
+}
diff --git a/kms/internal/infrastructure/store/route_test.go b/kms/internal/infrastructure/store/route_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..a86df2f06cae42936fbc3230563e0d73840261df
--- /dev/null
+++ b/kms/internal/infrastructure/store/route_test.go
@@ -0,0 +1,220 @@
+package store
+
+import (
+	"context"
+	"testing"
+
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+	"code.fbi.h-da.de/danet/costaquanta/libs/logging"
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.opentelemetry.io/otel/trace/noop"
+)
+
+func createCtxAndStoreForTest() (*InMemoryRouteStore, context.Context) {
+	tracer := noop.NewTracerProvider().Tracer("routeStoreTest")
+
+	parentLogger := logging.CreateProductionLogger("kms")
+	childLogger := logging.CreateChildLogger(parentLogger, "routeStoreTest")
+
+	ctx := context.Background()
+
+	return NewInMemoryRouteStore(childLogger, tracer), ctx
+}
+
+func TestInMemoryRouteStore_AddRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Add non-existing route to store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+
+		addedRoute, err := store.AddRoute(ctx, route)
+		require.NoError(t, err)
+		assert.Equal(t, route, addedRoute)
+		assert.Equal(t, route, store.dataStore[route.ID])
+	})
+
+	t.Run("Add existing route to store should fail", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route.ID] = route
+
+		addedRoute, err := store.AddRoute(ctx, route)
+		require.Error(t, err)
+		assert.Equal(t, model.Route{}, addedRoute)
+	})
+}
+
+func TestInMemoryRouteStore_DeleteRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Delete existing route from store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route.ID] = route
+
+		err := store.DeleteRoute(ctx, route.ID.String())
+		require.NoError(t, err)
+
+		assert.NotContains(t, store.dataStore, route.ID)
+	})
+
+	t.Run("Delete non-existing route from store should fail", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		err := store.DeleteRoute(ctx, uuid.New().String())
+		require.Error(t, err)
+	})
+
+	t.Run("Delete existing route twice from store should fail", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route.ID] = route
+
+		err := store.DeleteRoute(ctx, route.ID.String())
+		require.NoError(t, err)
+
+		err = store.DeleteRoute(ctx, route.ID.String())
+		require.Error(t, err)
+	})
+}
+
+func TestInMemoryRouteStore_GetRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Get existing route from store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route.ID] = route
+
+		gotRoute, err := store.GetRoute(ctx, route.ID.String())
+		require.NoError(t, err)
+		assert.Equal(t, route, gotRoute)
+	})
+
+	t.Run("Get non-existing route from store should fail", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		_, err := store.GetRoute(ctx, uuid.New().String())
+		require.Error(t, err)
+	})
+}
+
+func TestInMemoryRouteStore_GetRouteList(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Get empty route list from store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		routes, err := store.GetRouteList(ctx)
+		require.NoError(t, err)
+		assert.Empty(t, routes)
+	})
+
+	t.Run("Get non-empty route list from store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route1 := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		route2 := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1338",
+			PrevHopId:       "43",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route1.ID] = route1
+		store.dataStore[route2.ID] = route2
+
+		routes, err := store.GetRouteList(ctx)
+		require.NoError(t, err)
+
+		assert.Len(t, routes, 2)
+	})
+}
+
+func TestInMemoryRouteStore_UpdateRoute(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Update existing route in store", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+		store.dataStore[route.ID] = route
+
+		updatedRoute := route
+		updatedRoute.NextHopId = "1338"
+
+		err := store.UpdateRoute(ctx, updatedRoute)
+		require.NoError(t, err)
+
+		gotRoute, err := store.GetRoute(ctx, updatedRoute.ID.String())
+		require.NoError(t, err)
+		assert.Equal(t, updatedRoute, gotRoute)
+	})
+
+	t.Run("Update non-existing route in store should fail", func(t *testing.T) {
+		t.Parallel()
+		store, ctx := createCtxAndStoreForTest()
+
+		route := model.Route{
+			ID:              uuid.New(),
+			NextHopId:       "1337",
+			PrevHopId:       "42",
+			CryptoAlgorithm: model.CryptoAlgorithm_CRYPTO_ALGORITHM_AES_256_GCM,
+		}
+
+		err := store.UpdateRoute(ctx, route)
+		require.Error(t, err)
+	})
+}
diff --git a/kms/internal/service/routing.go b/kms/internal/service/routing.go
new file mode 100644
index 0000000000000000000000000000000000000000..86e4cc398bb708825fdfede7c8b1cc78699ec02c
--- /dev/null
+++ b/kms/internal/service/routing.go
@@ -0,0 +1,93 @@
+package service
+
+import (
+	"context"
+
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/model"
+	"code.fbi.h-da.de/danet/costaquanta/kms/internal/core/ports"
+	"go.opentelemetry.io/otel/trace"
+	"go.uber.org/zap"
+)
+
+type Routing struct {
+	logger *zap.SugaredLogger
+	tracer trace.Tracer
+
+	store ports.RouteStore
+}
+
+func NewRoutingService(
+	logger *zap.SugaredLogger,
+	tracer trace.Tracer,
+	store ports.RouteStore,
+) *Routing {
+	return &Routing{
+		logger: logger,
+		tracer: tracer,
+
+		store: store,
+	}
+}
+
+func (r *Routing) AddRoute(
+	ctx context.Context,
+	route model.Route,
+) (model.Route, error) {
+	_, span := r.tracer.Start(ctx, "add-route")
+	defer span.End()
+
+	sr, err := r.store.AddRoute(ctx, route)
+	if err != nil {
+		return model.Route{}, err
+	}
+
+	return sr, nil
+}
+
+func (r *Routing) DeleteRoute(ctx context.Context, routeID string) error {
+	_, span := r.tracer.Start(ctx, "delete-route")
+	defer span.End()
+
+	err := r.store.DeleteRoute(ctx, routeID)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (r *Routing) GetRoute(ctx context.Context, routeID string) (model.Route, error) {
+	_, span := r.tracer.Start(ctx, "get-route")
+	defer span.End()
+
+	route, err := r.store.GetRoute(ctx, routeID)
+	if err != nil {
+		return model.Route{}, err
+	}
+
+	return route, nil
+}
+
+func (r *Routing) GetRouteList(ctx context.Context) ([]model.Route, error) {
+	_, span := r.tracer.Start(ctx, "get-route-list")
+	defer span.End()
+
+	routes, err := r.store.GetRouteList(ctx)
+	if err != nil {
+		return nil, err
+	}
+
+	return routes, nil
+}
+
+func (r *Routing) UpdateRoute(ctx context.Context, route model.Route) error {
+	_, span := r.tracer.Start(ctx, "update-route")
+	defer span.End()
+
+	err := r.store.UpdateRoute(ctx, route)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}