diff --git a/Makefile b/Makefile
index edffb61feca4e592fbd263981a3ebcf5ea682477..9a64367046d3ac93fdf4ec05437cfb15ffc43777 100644
--- a/Makefile
+++ b/Makefile
@@ -1,3 +1,7 @@
+.PHONY: proto logging mocks .FORCE
+
+SHELL = bash
+
 PROTODIR=perxis-proto/proto
 DSTDIR=./proto
 ALLPROTO?=$(shell find $(PROTODIR) -name '*.proto' )
@@ -6,6 +10,10 @@ PROTOFILES=	$(filter-out $(PROTODIR)/status/status.proto, $(ALLPROTO))
 PROTOGOFILES=$(PROTOFILES:.proto=.pb.go)
 PROTOGOGRPCFILES=$(PROTOFILES:.proto=_grpc.pb.go)
 
+PKGDIR=pkg
+ACCESSLOGGING=$(shell find $(PKGDIR) -name "logging_middleware.go" -type f)
+ERRORLOGGING=$(shell find $(PKGDIR) -name "error_logging_middleware.go" -type f)
+
 # Генерация grpc-клиентов для go
 proto: protoc-check protoc-gen-go-check $(PROTOGOFILES)
 	@echo "Generated all protobuf Go files"
@@ -33,11 +41,18 @@ ifeq (,$(wildcard $(GOPATH)/bin/protoc-gen-go))
 	or visit \"https://github.com/golang/protobuf/tree/v1.3.2#installation\" for more.\n")
 endif
 
+# Генерация логгирования (access & error) для всех сервисов. Предполагается наличие файлов `logging_middleware.go/error_middleware.go`
+# с директивой go:generate и командой генерации кода в директориях `/pkg` сервисов
+# Для установки инструмента генерации выполнить команду `go get -u github.com/hexdigest/gowrap/cmd/gowrap`
+logging: $(ERRORLOGGING) $(ACCESSLOGGING)
 
+%/middleware/logging_middleware.go: .FORCE
+	@echo "$@"
+	@go generate "$@"
 
-#MICROGENFILES?=$(shell find $(SERVICESDIR) -name "service.go" -exec grep -Ril "microgen" {} \;)
-#SERVICEDIRS?=$(shell find $(SERVICESDIR) -name "service" -type d -exec dirname {} \;)
-#SERVICEFILES?=$(shell find $(SERVICESDIR) -name "service.go" -exec grep -Ril "go:generate" {} \;)
+%/middleware/error_logging_middleware.go: .FORCE
+	@echo "$@"
+	@go generate "$@"
 
 # Генерация моков для всех интерфейсов, найденных в директории. Выходные файлы с моками сохраняются в `./mocks`
 MOCKSDIRS?=$(shell find . -name "service.go" -exec dirname {} \;)
diff --git a/assets/templates/middleware/access_log b/assets/templates/middleware/access_log
new file mode 100644
index 0000000000000000000000000000000000000000..a8587b82d5a72130690a61c81e9f78e5eeb6e726
--- /dev/null
+++ b/assets/templates/middleware/access_log
@@ -0,0 +1,64 @@
+import (
+  "fmt"
+  "time"
+  "context"
+
+  "go.uber.org/zap"
+)
+
+{{ $funcName := (or .Vars.FuncName ("LoggingMiddleware")) }}
+{{ $decorator := (or .Vars.DecoratorName ("loggingMiddleware")) }}
+
+// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging
+type {{$decorator}} struct {
+  logger *zap.Logger
+  next {{.Interface.Type}}
+}
+
+// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging
+func {{$funcName}}(logger *zap.Logger) Middleware {
+  return func(next {{.Interface.Type}}) {{.Interface.Type}} {
+    return &{{$decorator}}{
+      next: next,
+      logger: logger,
+    }
+  }
+}
+
+{{range $method := .Interface.Methods}}
+    func (m *{{$decorator}}) {{$method.Declaration}} {
+        begin := time.Now()
+        {{- if $method.HasParams}}
+        var fields []zapcore.Field
+        for k, v := range {{$method.ParamsMap}} {
+            if k == "ctx" {
+			    fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+                continue
+            }
+            fields = append(fields, zap.Reflect(k,v))
+        }
+        {{end}}
+
+		m.logger.Debug("{{$method.Name}}.Request",fields...)
+
+        {{ $method.ResultsNames }} = m.next.{{ $method.Call }}
+
+        fields = []zapcore.Field{
+			zap.Duration("time", time.Since(begin)),
+			zap.Error(err),
+        }
+
+        {{ if $method.HasResults}}
+        for k, v := range {{$method.ResultsMap}} {
+            if k == "err" {
+                continue
+            }
+            fields = append(fields, zap.Reflect(k,v))
+        }
+        {{end}}
+
+		m.logger.Debug("{{$method.Name}}.Response", fields...)
+
+        return {{ $method.ResultsNames }}
+    }
+{{end}}
diff --git a/assets/templates/middleware/error_log b/assets/templates/middleware/error_log
new file mode 100755
index 0000000000000000000000000000000000000000..9455e907b738801eb7f2d43d428d98cc620370a0
--- /dev/null
+++ b/assets/templates/middleware/error_log
@@ -0,0 +1,40 @@
+import (
+  "io"
+  "time"
+
+  "go.uber.org/zap"
+)
+
+{{ $funcName := (or .Vars.FuncName ("ErrorLoggingMiddleware")) }}
+{{ $decorator := (or .Vars.DecoratorName ("errorLoggingMiddleware")) }}
+
+// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging
+type {{$decorator}} struct {
+  logger *zap.Logger
+  next {{.Interface.Type}}
+}
+
+// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging
+func {{$funcName}}(logger *zap.Logger) Middleware {
+  return func(next {{.Interface.Type}}) {{.Interface.Type}} {
+    return &{{$decorator}}{
+      next: next,
+      logger: logger,
+    }
+  }
+}
+
+{{range $method := .Interface.Methods}}
+    func (m *{{$decorator}}) {{$method.Declaration}} {
+        logger := m.logger
+        {{- if $method.ReturnsError}}
+            defer func() {
+                if err != nil {
+      		        logger.Warn("response error", zap.Error(err))
+      		    }
+      	    }()
+        {{end -}}
+
+        {{ $method.Pass "m.next." }}
+    }
+{{end}}
diff --git a/assets/templates/middleware/middleware b/assets/templates/middleware/middleware
new file mode 100755
index 0000000000000000000000000000000000000000..89877774c933840c2bdd569f2beed8105588aae2
--- /dev/null
+++ b/assets/templates/middleware/middleware
@@ -0,0 +1,21 @@
+import (
+	"go.uber.org/zap"
+)
+
+type Middleware func({{.Interface.Type}}) {{.Interface.Type}}
+
+
+func WithLog(s {{.Interface.Type}}, logger *zap.Logger, log_access bool) {{.Interface.Type}} {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("{{ .Interface.Name }}")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
+
diff --git a/assets/templates/middleware/recovery b/assets/templates/middleware/recovery
new file mode 100644
index 0000000000000000000000000000000000000000..a84fa3f913e885a1c9b8f1ed71848856137a92fe
--- /dev/null
+++ b/assets/templates/middleware/recovery
@@ -0,0 +1,38 @@
+import (
+	"go.uber.org/zap"
+)
+
+{{ $funcName := (or .Vars.FuncName ("RecoveringMiddleware")) }}
+{{ $decorator := (or .Vars.DecoratorName ("recoveringMiddleware")) }}
+
+// {{$decorator}} implements {{.Interface.Type}} that is instrumented with logging
+type {{$decorator}} struct {
+  logger *zap.Logger
+  next {{.Interface.Type}}
+}
+
+// {{$funcName}} instruments an implementation of the {{.Interface.Type}} with simple logging
+func {{$funcName}}(logger *zap.Logger) Middleware {
+  return func(next {{.Interface.Type}}) {{.Interface.Type}} {
+    return &{{$decorator}}{
+      next: next,
+      logger: logger,
+    }
+  }
+}
+
+{{range $method := .Interface.Methods}}
+func (m *{{$decorator}}) {{$method.Declaration}} {
+    logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			{{- if $method.ReturnsError}}
+			err = fmt.Errorf("%v", r)
+			{{end -}}
+		}
+	}()
+
+	{{ $method.Pass "m.next." }}
+}
+{{end}}
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 05e37d08d81a77ffc8319f1094590b4ec4f236ab..c4bc532e1b85714da9e5a283f5b60f65748fa1f5 100644
--- a/go.mod
+++ b/go.mod
@@ -17,28 +17,38 @@ require (
 	github.com/stretchr/testify v1.8.0
 	go.mongodb.org/mongo-driver v1.11.4
 	go.uber.org/zap v1.19.1
-	golang.org/x/crypto v0.5.0
-	golang.org/x/net v0.5.0
-	google.golang.org/grpc v1.45.0
-	google.golang.org/protobuf v1.28.0
+	golang.org/x/crypto v0.8.0
+	golang.org/x/net v0.9.0
+	google.golang.org/grpc v1.54.0
+	google.golang.org/protobuf v1.28.1
 	gopkg.in/yaml.v3 v3.0.1
 )
 
 require (
+	github.com/Masterminds/goutils v1.1.1 // indirect
+	github.com/Masterminds/semver/v3 v3.2.1 // indirect
+	github.com/Masterminds/sprig/v3 v3.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/go-kit/log v0.2.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
 	github.com/golang/snappy v0.0.1 // indirect
-	github.com/google/go-cmp v0.5.7 // indirect
+	github.com/google/uuid v1.3.0 // indirect
 	github.com/gosimple/unidecode v1.0.1 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
+	github.com/hexdigest/gowrap v1.3.2 // indirect
+	github.com/huandu/xstrings v1.4.0 // indirect
+	github.com/imdario/mergo v0.3.15 // indirect
 	github.com/klauspost/compress v1.13.6 // indirect
+	github.com/mitchellh/copystructure v1.2.0 // indirect
+	github.com/mitchellh/reflectwalk v1.0.2 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
 	github.com/nats-io/nkeys v0.3.0 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/shopspring/decimal v1.3.1 // indirect
+	github.com/spf13/cast v1.5.0 // indirect
 	github.com/stretchr/objx v0.4.0 // indirect
 	github.com/xdg-go/pbkdf2 v1.0.0 // indirect
 	github.com/xdg-go/scram v1.1.1 // indirect
@@ -46,8 +56,10 @@ require (
 	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	go.uber.org/atomic v1.9.0 // indirect
 	go.uber.org/multierr v1.7.0 // indirect
-	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
-	golang.org/x/sys v0.4.0 // indirect
-	golang.org/x/text v0.6.0 // indirect
-	google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 // indirect
+	golang.org/x/mod v0.10.0 // indirect
+	golang.org/x/sync v0.1.0 // indirect
+	golang.org/x/sys v0.7.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
+	golang.org/x/tools v0.8.0 // indirect
+	google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect
 )
diff --git a/go.sum b/go.sum
index 1b2e49b505ab646f23e970362f978a04fae0a2b4..53493f56d5ddbe17ec5350541663552f0dcb311c 100644
--- a/go.sum
+++ b/go.sum
@@ -1,86 +1,59 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
-github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
+github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
+github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
+github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
+github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
 github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU=
 github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8=
 github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
-github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
-github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
-github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
 github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
-github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4=
 github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
 github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw=
 github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
 github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
 github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
-github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
 github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
 github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
-github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/gosimple/slug v1.13.1 h1:bQ+kpX9Qa6tHRaK+fZR0A0M2Kd7Pa5eHPPsb1JpHD+Q=
 github.com/gosimple/slug v1.13.1/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
 github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
 github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
-github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
 github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
 github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
+github.com/hexdigest/gowrap v1.3.2 h1:ZDhDFhrbAHYRdt9ZnULKZyggC/3+W9EpfX6R8DjlggY=
+github.com/hexdigest/gowrap v1.3.2/go.mod h1:g8N2jI4n9AKrf843erksNTrt4sdkG+TGVfhWe8dWrJQ=
+github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
+github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
+github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
+github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM=
+github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
 github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
 github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
 github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
@@ -100,8 +73,14 @@ github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i
 github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
 github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
 github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g=
+github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
+github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
+github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
 github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
 github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
+github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
+github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -123,18 +102,22 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84=
 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
 github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
 github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
+github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w=
+github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -153,9 +136,9 @@ github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgk
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d h1:splanxYIlg+5LfHAM6xpdFEAYOk8iySO56hMFq6uLyA=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.mongodb.org/mongo-driver v1.11.4 h1:4ayjakA013OdpGyL2K3ZqylTac/rMjrJOMZ1EHizXas=
 go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
-go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -168,121 +151,94 @@ go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI=
 go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
 golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
 golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
-golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk=
+golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
 golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
-golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
-golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
-golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y=
+golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4 h1:ysnBoUyeL/H6RCvNRhWHjKoDEmguI+mPU+qHgK8qv/w=
-google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
-google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
-google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
-google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w=
+google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
+google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
+google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
-google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
+google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/clients/middleware/caching_middleware.go b/pkg/clients/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1733c0de4a8dde8f011d46b2b77005f8924e60bb
--- /dev/null
+++ b/pkg/clients/middleware/caching_middleware.go
@@ -0,0 +1,165 @@
+package middleware
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/clients"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Clients) service.Clients {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Clients
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, client *service.Client) (cl *service.Client, err error) {
+
+	cl, err = m.next.Create(ctx, client)
+	if err == nil {
+		m.cache.Remove(cl.SpaceID)
+	}
+	return cl, err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId string, id string) (cl *service.Client, err error) {
+
+	key := makeKey(spaceId, id)
+	value, e := m.cache.Get(key)
+	if e == nil {
+		return value.(*service.Client), err
+	}
+	cl, err = m.next.Get(ctx, spaceId, id)
+	if err == nil {
+		m.cache.Set(key, cl)
+		for _, key := range keysFromIdentities(spaceId, cl) {
+			m.cache.Set(key, cl)
+		}
+	}
+	return cl, err
+}
+
+func (m cachingMiddleware) GetBy(ctx context.Context, spaceId string, params *service.GetByParams) (cl *service.Client, err error) {
+	if params == nil {
+		return m.next.GetBy(ctx, spaceId, params)
+	}
+
+	key := getIdentKey(spaceId, params)
+	value, e := m.cache.Get(key)
+	if e == nil {
+		return value.(*service.Client), err
+	}
+	cl, err = m.next.GetBy(ctx, spaceId, params)
+	if err == nil {
+		m.cache.Set(makeKey(spaceId, cl.ID), cl)
+		for _, key := range keysFromIdentities(spaceId, cl) {
+			m.cache.Set(key, cl)
+		}
+	}
+	return cl, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, spaceId string) (clients []*service.Client, err error) {
+
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.([]*service.Client), err
+	}
+	clients, err = m.next.List(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, clients)
+	}
+	return clients, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, client *service.Client) (err error) {
+
+	err = m.next.Update(ctx, client)
+
+	if err == nil {
+		m.cache.Remove(client.SpaceID)
+		value, e := m.cache.Get(makeKey(client.SpaceID, client.ID))
+		if e == nil {
+			client := value.(*service.Client)
+			m.cache.Remove(makeKey(client.SpaceID, client.ID))
+			for _, key := range keysFromIdentities(client.SpaceID, client) {
+				m.cache.Remove(key)
+			}
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) {
+
+	err = m.next.Delete(ctx, spaceId, id)
+	if err == nil {
+		value, e := m.cache.Get(makeKey(spaceId, id))
+		if e == nil {
+			client := value.(*service.Client)
+			m.cache.Remove(makeKey(client.SpaceID, client.ID))
+			for _, key := range keysFromIdentities(client.SpaceID, client) {
+				m.cache.Remove(key)
+			}
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) {
+
+	err = m.next.Enable(ctx, spaceId, id, enable)
+	if err == nil {
+		value, e := m.cache.Get(makeKey(spaceId, id))
+		if e == nil {
+			client := value.(*service.Client)
+			m.cache.Remove(makeKey(client.SpaceID, client.ID))
+			for _, key := range keysFromIdentities(client.SpaceID, client) {
+				m.cache.Remove(key)
+			}
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
+
+func keysFromIdentities(spaceID string, client *service.Client) []string {
+	res := make([]string, 0)
+	if client.APIKey != nil && client.APIKey.Key != "" {
+		res = append(res, makeKey(spaceID, "api-key", client.APIKey.Key))
+	}
+	if client.TLS != nil && client.TLS.Subject != "" {
+		res = append(res, makeKey(spaceID, "tls", client.TLS.Subject))
+	}
+	if client.OAuth != nil && client.OAuth.ClientID != "" {
+		res = append(res, makeKey(spaceID, "oauth", client.OAuth.ClientID))
+	}
+	return res
+}
+
+func getIdentKey(spaceID string, params *service.GetByParams) string {
+	switch {
+	case params.APIKey != "":
+		return makeKey(spaceID, "api-key", params.APIKey)
+	case params.TLSSubject != "":
+		return makeKey(spaceID, "tls", params.TLSSubject)
+	case params.OAuthClientID != "":
+		return makeKey(spaceID, "oauth", params.OAuthClientID)
+	default:
+		return ""
+	}
+}
diff --git a/pkg/clients/middleware/caching_middleware_test.go b/pkg/clients/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b842e1183a5425a4b3aec8d59b0720e5627767ed
--- /dev/null
+++ b/pkg/clients/middleware/caching_middleware_test.go
@@ -0,0 +1,382 @@
+package middleware
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	csmocks "git.perx.ru/perxis/perxis-go/pkg/clients/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestClientsCache(t *testing.T) {
+
+	const (
+		cltID    = "cltID"
+		spaceID  = "spaceID"
+		clientID = "123@client"
+		size     = 5
+		ttl      = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		cs := &csmocks.Clients{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, cltID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, cltID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса.")
+
+		v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кэша при запросе по ClientID.")
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("GetBy from cache", func(t *testing.T) {
+		cs := &csmocks.Clients{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("GetBy", mock.Anything, spaceID, &clients.GetByParams{OAuthClientID: clientID}).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once()
+
+		v1, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+		require.NoError(t, err)
+
+		v2, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса.")
+
+		v3, err := svc.Get(ctx, spaceID, cltID)
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кэша, после запроса Get.")
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		cs := &csmocks.Clients{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+		vl1, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+
+		vl2, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+
+		t.Run("After Update", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once()
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Update(ctx, &clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}})
+			require.NoError(t, err)
+
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once()
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			v4, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v4, "Ожидает что после обновления объект был удален из кэша и будет запрошен заново из сервиса.")
+
+			v5, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v5)
+			assert.Same(t, v4, v5, "Ожидается что после обновления объект был удален из кеша и после запроса Get в кеш попал объект запрошенный заново из сервиса.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Update(List)", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Update(ctx, &clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}})
+			require.NoError(t, err)
+
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_2", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}, nil).Once()
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Delete", mock.Anything, spaceID, cltID).Return(nil).Once()
+
+			err = svc.Delete(ctx, spaceID, cltID)
+			require.NoError(t, err)
+
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(nil, errNotFound).Once()
+			cs.On("GetBy", mock.Anything, spaceID, &clients.GetByParams{OAuthClientID: clientID}).Return(nil, errNotFound).Once()
+			cs.On("List", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+
+			_, err = svc.Get(ctx, spaceID, cltID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объект был удален из кэша и получена ошибка из сервиса.")
+
+			_, err = svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объект был удален из кэша и получена ошибка из сервиса.")
+
+			_, err = svc.List(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объекты были удалены из кэша.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Delete(List)", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Delete", mock.Anything, spaceID, cltID).Return(nil).Once()
+
+			err = svc.Delete(ctx, spaceID, cltID)
+			require.NoError(t, err)
+
+			cs.On("List", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+
+			_, err = svc.List(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаление из хранилища объекты были удалены из кэша.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1"}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+			assert.Len(t, vl2, 1, "Ожидается получение объектов из кэша.")
+
+			cs.On("Create", mock.Anything, mock.Anything).Return(&clients.Client{ID: "cltID2", SpaceID: spaceID, Name: "client_2"}, nil).Once()
+
+			_, err = svc.Create(ctx, &clients.Client{ID: "cltID2", SpaceID: spaceID, Name: "client_2"})
+			require.NoError(t, err)
+
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, Name: "client_1"}, {ID: "cltID2", SpaceID: spaceID, Name: "client_2"}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 2, "Ожидается что после создания нового объекта кеш будет очищен и объекты запрошены заново из сервиса.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Enable", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			tr := true
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}, nil).Once()
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			v3, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по ClientID.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Enable", mock.Anything, spaceID, cltID, tr).Return(nil).Once()
+
+			err = svc.Enable(ctx, spaceID, cltID, tr)
+			require.NoError(t, err)
+
+			fl := false
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}, nil).Once()
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}}, nil).Once()
+
+			v4, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v4, "Ожидается что после активации объект был удален из кэша и запрошен у сервиса.")
+
+			v5, err := svc.GetBy(ctx, spaceID, &clients.GetByParams{OAuthClientID: clientID})
+			assert.NotSame(t, v3, v5, "Ожидается что после активации объект был удален из кеша и после запроса Get в кеш попал объект запрошенный заново из сервиса.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что после активации объекта, кеш будет очищен и объекты будут запрошены заново из сервиса.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After Enable(List)", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			tr := true
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &tr}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша, после повторного запроса.")
+
+			cs.On("Enable", mock.Anything, spaceID, cltID, tr).Return(nil).Once()
+
+			err = svc.Enable(ctx, spaceID, cltID, tr)
+			require.NoError(t, err)
+
+			fl := false
+			cs.On("List", mock.Anything, spaceID).Return([]*clients.Client{{ID: cltID, SpaceID: spaceID, OAuth: &clients.OAuth{ClientID: clientID, AuthID: "authID"}, Disabled: &fl}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что после активации объекта, кеш будет очищен и объекты будут запрошены заново из сервиса.")
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			cs := &csmocks.Clients{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID}}, nil).Once()
+			cs.On("Get", mock.Anything, spaceID, cltID).Return(&clients.Client{ID: cltID, SpaceID: spaceID, Name: "client_1", OAuth: &clients.OAuth{ClientID: clientID}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			v2, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша после повторного запроса.")
+
+			time.Sleep(2 * ttl)
+
+			v3, err := svc.Get(ctx, spaceID, cltID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что элемент был удален из кэша по истечению ttl и будет запрошен заново из сервиса.")
+
+			cs.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/clients/middleware/error_logging_middleware.go b/pkg/clients/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..0b96827a0c620ad1ca1aa5aaf6b93a821af6279d
--- /dev/null
+++ b/pkg/clients/middleware/error_logging_middleware.go
@@ -0,0 +1,100 @@
+package middleware
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements clients.Clients that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   clients.Clients
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the clients.Clients with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next clients.Clients) clients.Clients {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, client)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, id)
+}
+
+func (m *errorLoggingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Enable(ctx, spaceId, id, enable)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, id)
+}
+
+func (m *errorLoggingMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.GetBy(ctx, spaceId, params)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, client *clients.Client) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, client)
+}
diff --git a/pkg/clients/middleware/logging_middleware.go b/pkg/clients/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..ef3ea5947a637bfa9caffa3af64f1f7cd2b8e019
--- /dev/null
+++ b/pkg/clients/middleware/logging_middleware.go
@@ -0,0 +1,288 @@
+package middleware
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements clients.Clients that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   clients.Clients
+}
+
+// LoggingMiddleware instruments an implementation of the clients.Clients with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next clients.Clients) clients.Clients {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"client": client} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, client)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"id":      id} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, id)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"id":      id,
+		"enable":  enable} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Enable.Request", fields...)
+
+	err = m.next.Enable(ctx, spaceId, id, enable)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Enable.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"id":      id} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	client, err = m.next.Get(ctx, spaceId, id)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"client": client,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return client, err
+}
+
+func (m *loggingMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"params":  params} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetBy.Request", fields...)
+
+	client, err = m.next.GetBy(ctx, spaceId, params)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"client": client,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetBy.Response", fields...)
+
+	return client, err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	clients, err = m.next.List(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"clients": clients,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return clients, err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, client *clients.Client) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"client": client} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, client)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/clients/middleware/middleware.go b/pkg/clients/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..a49c9b3ebb0c041c23178d457ea5ddf2d2357d91
--- /dev/null
+++ b/pkg/clients/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"go.uber.org/zap"
+)
+
+type Middleware func(clients.Clients) clients.Clients
+
+func WithLog(s clients.Clients, logger *zap.Logger, log_access bool) clients.Clients {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Clients")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/clients/middleware/recovering_middleware.go b/pkg/clients/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..2406ca994112a148e6a204589ee1844a782922fc
--- /dev/null
+++ b/pkg/clients/middleware/recovering_middleware.go
@@ -0,0 +1,115 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package middleware
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/clients -i Clients -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/clients"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements clients.Clients that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   clients.Clients
+}
+
+// RecoveringMiddleware instruments an implementation of the clients.Clients with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next clients.Clients) clients.Clients {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, client *clients.Client) (created *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, client)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, id string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, id)
+}
+
+func (m *recoveringMiddleware) Enable(ctx context.Context, spaceId string, id string, enable bool) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Enable(ctx, spaceId, id, enable)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, id string) (client *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, id)
+}
+
+func (m *recoveringMiddleware) GetBy(ctx context.Context, spaceId string, params *clients.GetByParams) (client *clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.GetBy(ctx, spaceId, params)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (clients []*clients.Client, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, client *clients.Client) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, client)
+}
diff --git a/pkg/collaborators/middleware/caching_middleware.go b/pkg/collaborators/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..f57995acf8387c80edc622313ba4729ae11bc6f6
--- /dev/null
+++ b/pkg/collaborators/middleware/caching_middleware.go
@@ -0,0 +1,88 @@
+package service
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/collaborators"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Collaborators) service.Collaborators {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Collaborators
+}
+
+func (m cachingMiddleware) Set(ctx context.Context, spaceId, subject, role string) (err error) {
+
+	err = m.next.Set(ctx, spaceId, subject, role)
+	if err == nil {
+		m.cache.Remove(spaceId)
+		m.cache.Remove(subject)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId, subject string) (role string, err error) {
+
+	key := makeKey(spaceId, subject)
+	value, e := m.cache.Get(key)
+	if e == nil {
+		return value.(string), err
+	}
+	role, err = m.next.Get(ctx, spaceId, subject)
+	if err == nil {
+		m.cache.Set(key, role)
+	}
+	return role, err
+}
+
+func (m cachingMiddleware) Remove(ctx context.Context, spaceId, subject string) (err error) {
+
+	err = m.next.Remove(ctx, spaceId, subject)
+	if err == nil {
+		m.cache.Remove(makeKey(spaceId, subject))
+		m.cache.Remove(spaceId)
+		m.cache.Remove(subject)
+	}
+	return err
+}
+
+func (m cachingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*service.Collaborator, err error) {
+
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.([]*service.Collaborator), err
+	}
+	collaborators, err = m.next.ListCollaborators(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, collaborators)
+	}
+	return collaborators, err
+}
+
+func (m cachingMiddleware) ListSpaces(ctx context.Context, subject string) (collaborators []*service.Collaborator, err error) {
+
+	value, e := m.cache.Get(subject)
+	if e == nil {
+		return value.([]*service.Collaborator), err
+	}
+	collaborators, err = m.next.ListSpaces(ctx, subject)
+	if err == nil {
+		m.cache.Set(subject, collaborators)
+	}
+	return collaborators, err
+}
diff --git a/pkg/collaborators/middleware/caching_middleware_test.go b/pkg/collaborators/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..da1d6e842f542e1c664f988f38c4797ea31c38f1
--- /dev/null
+++ b/pkg/collaborators/middleware/caching_middleware_test.go
@@ -0,0 +1,190 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	csmocks "git.perx.ru/perxis/perxis-go/pkg/collaborators/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCollaboratorsCache(t *testing.T) {
+
+	const (
+		userID    = "userID"
+		spaceID   = "spaceID"
+		spaceRole = "spaceRole"
+		size      = 5
+		ttl       = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		cs := &csmocks.Collaborators{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once()
+
+		_, err := svc.Get(ctx, spaceID, userID)
+		require.NoError(t, err)
+
+		rl, err := svc.Get(ctx, spaceID, userID)
+		require.NoError(t, err)
+		assert.Equal(t, spaceRole, rl)
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("ListCollaborators from cache", func(t *testing.T) {
+		cs := &csmocks.Collaborators{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+
+		v1, err := svc.ListCollaborators(ctx, spaceID)
+		require.NoError(t, err)
+		v2, err := svc.ListCollaborators(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, v1[0], v2[0], "Ожидается получение объектов из кэша при повторном запросе.")
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("ListSpaces from cache", func(t *testing.T) {
+		cs := &csmocks.Collaborators{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+		cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+
+		v1, err := svc.ListSpaces(ctx, userID)
+		require.NoError(t, err)
+		v2, err := svc.ListSpaces(ctx, userID)
+		require.NoError(t, err)
+		assert.Same(t, v1[0], v2[0], "Ожидается получение объектов из кэша при повторном запросе.")
+
+		cs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Remove", func(t *testing.T) {
+			cs := &csmocks.Collaborators{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once()
+			cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+			cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+
+			_, err := svc.Get(ctx, spaceID, userID)
+			require.NoError(t, err)
+
+			rl, err := svc.Get(ctx, spaceID, userID)
+			require.NoError(t, err)
+			assert.Equal(t, spaceRole, rl, "Ожидается получение данных из кэша.")
+
+			lc1, err := svc.ListCollaborators(ctx, spaceID)
+			require.NoError(t, err)
+			lc2, err := svc.ListCollaborators(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, lc1[0], lc2[0], "Ожидается получение объектов из кэша.")
+
+			ls1, err := svc.ListSpaces(ctx, userID)
+			require.NoError(t, err)
+			ls2, err := svc.ListSpaces(ctx, userID)
+			require.NoError(t, err)
+			assert.Same(t, ls1[0], ls2[0], "Ожидается получение объектов из кэша.")
+
+			cs.On("Remove", mock.Anything, spaceID, userID).Return(nil).Once()
+
+			cs.On("Get", mock.Anything, spaceID, userID).Return("", errNotFound).Once()
+			cs.On("ListCollaborators", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+			cs.On("ListSpaces", mock.Anything, userID).Return(nil, errNotFound).Once()
+
+			err = svc.Remove(ctx, spaceID, userID)
+
+			rl, err = svc.Get(ctx, spaceID, userID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Empty(t, rl)
+
+			lc, err := svc.ListCollaborators(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Nil(t, lc)
+
+			ls, err := svc.ListSpaces(ctx, userID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Nil(t, ls)
+
+			cs.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			cs := &csmocks.Collaborators{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(cs)
+
+			cs.On("Get", mock.Anything, spaceID, userID).Return(spaceRole, nil).Once()
+			cs.On("ListCollaborators", mock.Anything, spaceID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+			cs.On("ListSpaces", mock.Anything, userID).Return([]*collaborators.Collaborator{{SpaceID: spaceID, Subject: userID, Role: spaceRole}}, nil).Once()
+
+			_, err := svc.Get(ctx, spaceID, userID)
+			require.NoError(t, err)
+
+			rl, err := svc.Get(ctx, spaceID, userID)
+			require.NoError(t, err)
+			assert.Equal(t, spaceRole, rl, "Ожидается получение данных из кэша.")
+
+			lc1, err := svc.ListCollaborators(ctx, spaceID)
+			require.NoError(t, err)
+			lc2, err := svc.ListCollaborators(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, lc1[0], lc2[0], "Ожидается получение объектов из кэша.")
+
+			ls1, err := svc.ListSpaces(ctx, userID)
+			require.NoError(t, err)
+			ls2, err := svc.ListSpaces(ctx, userID)
+			require.NoError(t, err)
+			assert.Same(t, ls1[0], ls2[0], "Ожидается получение объектов из кэша.")
+
+			cs.On("Remove", mock.Anything, spaceID, userID).Return(nil).Once()
+
+			cs.On("Get", mock.Anything, spaceID, userID).Return("", errNotFound).Once()
+			cs.On("ListCollaborators", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+			cs.On("ListSpaces", mock.Anything, userID).Return(nil, errNotFound).Once()
+
+			err = svc.Remove(ctx, spaceID, userID)
+
+			rl, err = svc.Get(ctx, spaceID, userID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Empty(t, rl)
+
+			lc, err := svc.ListCollaborators(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Nil(t, lc)
+
+			ls, err := svc.ListSpaces(ctx, userID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление данных из кеша, и получение ошибки от сервиса")
+			assert.Nil(t, ls)
+
+			cs.AssertExpectations(t)
+		})
+	})
+
+}
diff --git a/pkg/collaborators/middleware/error_logging_middleware.go b/pkg/collaborators/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..a45dfd8053e1718fdc139f1792a9f0b1547c08d1
--- /dev/null
+++ b/pkg/collaborators/middleware/error_logging_middleware.go
@@ -0,0 +1,80 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements collaborators.Collaborators that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   collaborators.Collaborators
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the collaborators.Collaborators with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next collaborators.Collaborators) collaborators.Collaborators {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, subject)
+}
+
+func (m *errorLoggingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.ListCollaborators(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.ListSpaces(ctx, subject)
+}
+
+func (m *errorLoggingMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Remove(ctx, spaceId, subject)
+}
+
+func (m *errorLoggingMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Set(ctx, spaceId, subject, role)
+}
diff --git a/pkg/collaborators/middleware/logging_middleware.go b/pkg/collaborators/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..41f541e6cc0e09f2fc6240d585159bda09c10a74
--- /dev/null
+++ b/pkg/collaborators/middleware/logging_middleware.go
@@ -0,0 +1,216 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements collaborators.Collaborators that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   collaborators.Collaborators
+}
+
+// LoggingMiddleware instruments an implementation of the collaborators.Collaborators with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next collaborators.Collaborators) collaborators.Collaborators {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"subject": subject} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	role, err = m.next.Get(ctx, spaceId, subject)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"role": role,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return role, err
+}
+
+func (m *loggingMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListCollaborators.Request", fields...)
+
+	collaborators, err = m.next.ListCollaborators(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"collaborators": collaborators,
+		"err":           err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListCollaborators.Response", fields...)
+
+	return collaborators, err
+}
+
+func (m *loggingMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"subject": subject} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListSpaces.Request", fields...)
+
+	spaces, err = m.next.ListSpaces(ctx, subject)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"spaces": spaces,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListSpaces.Response", fields...)
+
+	return spaces, err
+}
+
+func (m *loggingMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"subject": subject} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Remove.Request", fields...)
+
+	err = m.next.Remove(ctx, spaceId, subject)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Remove.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"subject": subject,
+		"role":    role} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Set.Request", fields...)
+
+	err = m.next.Set(ctx, spaceId, subject, role)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Set.Response", fields...)
+
+	return err
+}
diff --git a/pkg/collaborators/middleware/middleware.go b/pkg/collaborators/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..682fd963ae290298adaec9001b7f60215c80d4db
--- /dev/null
+++ b/pkg/collaborators/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"go.uber.org/zap"
+)
+
+type Middleware func(collaborators.Collaborators) collaborators.Collaborators
+
+func WithLog(s collaborators.Collaborators, logger *zap.Logger, log_access bool) collaborators.Collaborators {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Collaborators")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/collaborators/middleware/recovering_middleware.go b/pkg/collaborators/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..84e9dfb59514b1a146c96f0239cd3c8e83d8e7ba
--- /dev/null
+++ b/pkg/collaborators/middleware/recovering_middleware.go
@@ -0,0 +1,91 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collaborators -i Collaborators -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collaborators"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements collaborators.Collaborators that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   collaborators.Collaborators
+}
+
+// RecoveringMiddleware instruments an implementation of the collaborators.Collaborators with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next collaborators.Collaborators) collaborators.Collaborators {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, subject string) (role string, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, subject)
+}
+
+func (m *recoveringMiddleware) ListCollaborators(ctx context.Context, spaceId string) (collaborators []*collaborators.Collaborator, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.ListCollaborators(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) ListSpaces(ctx context.Context, subject string) (spaces []*collaborators.Collaborator, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.ListSpaces(ctx, subject)
+}
+
+func (m *recoveringMiddleware) Remove(ctx context.Context, spaceId string, subject string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Remove(ctx, spaceId, subject)
+}
+
+func (m *recoveringMiddleware) Set(ctx context.Context, spaceId string, subject string, role string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Set(ctx, spaceId, subject, role)
+}
diff --git a/pkg/collections/middleware/caching_middleware.go b/pkg/collections/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..0bb41c9e28027e8d9c4cfa5e305b91ac76f44165
--- /dev/null
+++ b/pkg/collections/middleware/caching_middleware.go
@@ -0,0 +1,136 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/collections"
+	envService "git.perx.ru/perxis/perxis-go/pkg/environments"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+)
+
+func makeKey(spaceId, envId, collectionId string, disableSchemaIncludes bool) string {
+	s := spaceId + "-" + envId + "-" + collectionId + "-"
+	if disableSchemaIncludes {
+		s += "1"
+	} else {
+		s += "0"
+	}
+	return s
+}
+
+func CachingMiddleware(cache *cache.Cache, envs envService.Environments) Middleware {
+	return func(next service.Collections) service.Collections {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+			envs:  envs,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Collections
+	envs  envService.Environments
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, collection *service.Collection) (coll *service.Collection, err error) {
+	return m.next.Create(ctx, collection)
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*service.GetOptions) (coll *service.Collection, err error) {
+
+	opts := service.MergeGetOptions(options...)
+	value, e := m.cache.Get(makeKey(spaceId, envId, collectionId, opts.DisableSchemaIncludes))
+	if e == nil {
+		return value.(*service.Collection), err
+	}
+	coll, err = m.next.Get(ctx, spaceId, envId, collectionId, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID)
+		if err != nil {
+			return nil, err
+		}
+		m.cache.Set(makeKey(coll.SpaceID, env.ID, coll.ID, opts.DisableSchemaIncludes), coll)
+		for _, al := range env.Aliases {
+			m.cache.Set(makeKey(coll.SpaceID, al, coll.ID, opts.DisableSchemaIncludes), coll)
+		}
+
+	}
+	return coll, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, spaceId, envId string, filter *service.Filter) (collections []*service.Collection, err error) {
+	return m.next.List(ctx, spaceId, envId, filter)
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, coll *service.Collection) (err error) {
+
+	err = m.next.Update(ctx, coll)
+	if err == nil {
+		env, err := m.envs.Get(ctx, coll.SpaceID, coll.EnvID)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, true))
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, coll.ID, false))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, true))
+			m.cache.Remove(makeKey(env.SpaceID, al, coll.ID, false))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) SetSchema(ctx context.Context, spaceId, envId, collectionId string, schema *schema.Schema) (err error) {
+	err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema)
+	if err == nil {
+		env, err := m.envs.Get(ctx, spaceId, envId)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true))
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true))
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) SetState(ctx context.Context, spaceId, envId, collectionId string, state *service.StateInfo) (err error) {
+	err = m.next.SetState(ctx, spaceId, envId, collectionId, state)
+	if err == nil {
+		env, err := m.envs.Get(ctx, spaceId, envId)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true))
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true))
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) {
+
+	err = m.next.Delete(ctx, spaceId, envId, collectionId)
+	if err == nil {
+		env, err := m.envs.Get(ctx, spaceId, envId)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, true))
+		m.cache.Remove(makeKey(env.SpaceID, env.ID, collectionId, false))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, true))
+			m.cache.Remove(makeKey(env.SpaceID, al, collectionId, false))
+		}
+	}
+	return err
+}
diff --git a/pkg/collections/middleware/caching_middleware_test.go b/pkg/collections/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..967a75bce7fa7d933918edc9f05600f75a6c0ff5
--- /dev/null
+++ b/pkg/collections/middleware/caching_middleware_test.go
@@ -0,0 +1,458 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	colsmocks "git.perx.ru/perxis/perxis-go/pkg/collections/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestCollections_Cache(t *testing.T) {
+
+	const (
+		colID    = "colID"
+		spaceID  = "spaceID"
+		envID    = "envId"
+		envAlias = "envAlias"
+		size     = 5
+		ttl      = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		col := &colsmocks.Collections{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envID, colID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envID, colID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша при повторном запросе по ID окружения.")
+
+		v3, err := svc.Get(ctx, spaceID, envAlias, colID)
+		require.NoError(t, err)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по alias окружения.")
+
+		env.AssertExpectations(t)
+		col.AssertExpectations(t)
+	})
+
+	t.Run("Get from cache(by Alias)", func(t *testing.T) {
+		col := &colsmocks.Collections{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+		col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envAlias, colID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envAlias, colID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша при повторном запросе по Alias окружения.")
+
+		v3, err := svc.Get(ctx, spaceID, envID, colID)
+		require.NoError(t, err)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по ID окружения.")
+
+		env.AssertExpectations(t)
+		col.AssertExpectations(t)
+	})
+
+	t.Run("Get from cache with options", func(t *testing.T) {
+		col := &colsmocks.Collections{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		col.On("Get", mock.Anything, spaceID, envID, colID, mock.Anything).Run(func(args mock.Arguments) {
+			require.Len(t, args, 5)
+			opt := args.Get(4).(*collections.GetOptions)
+			assert.True(t, opt.DisableSchemaIncludes)
+		}).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+
+		_, err := svc.Get(ctx, spaceID, envID, colID, []*collections.GetOptions{{DisableSchemaIncludes: true}}...)
+		require.NoError(t, err)
+
+		env.AssertExpectations(t)
+		col.AssertExpectations(t)
+	})
+
+	//t.Run("List from cache", func(t *testing.T) {
+	//	col := &colsmocks.Collections{}
+	//	env := &envmocks.Environments{}
+	//
+	//	svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+	//
+	//	col.On("List", mock.Anything, spaceID, envID).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+	//	env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+	//
+	//	vl1, err := svc.List(ctx, spaceID, envID, nil)
+	//	require.NoError(t, err)
+	//
+	//	vl2, err := svc.List(ctx, spaceID, envID, nil)
+	//	require.NoError(t, err)
+	//	assert.Len(t, vl2, 1)
+	//	assert.Same(t, vl1[0], vl2[0], "При повторном запросе по ID окружения, ожидается получение списка объектов из кеша.")
+	//
+	//	vl3, err := svc.List(ctx, spaceID, envAlias, nil)
+	//	require.NoError(t, err)
+	//	assert.Len(t, vl3, 1)
+	//	assert.Same(t, vl3[0], vl2[0], "При повторном запросе по Alias окружения, ожидается получение списка объектов из кеша.")
+	//
+	//	env.AssertExpectations(t)
+	//	col.AssertExpectations(t)
+	//})
+
+	t.Run("List", func(t *testing.T) {
+		col := &colsmocks.Collections{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+		col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+		col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+		_, err := svc.List(ctx, spaceID, envAlias, nil)
+		require.NoError(t, err)
+
+		_, err = svc.List(ctx, spaceID, envID, nil)
+		require.NoError(t, err)
+
+		env.AssertExpectations(t)
+		col.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Update", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl1, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once()
+
+			v4, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален из кэша и будет запрошен заново из сервиса.")
+
+			v5, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl2, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.NotSame(t, vl1[0], vl2[0], "Ожидает что после обновления элементы будут запрошены заново из сервиса.")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+
+		t.Run("After Update(by Alias)", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			//env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по ID окружения.")
+
+			vl1, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			err = svc.Update(ctx, &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			//env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once()
+
+			v4, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления был удален из кэша и будет запрошен заново из сервиса.")
+
+			v5, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl4, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.NotSame(t, vl1[0], vl4[0], "Ожидает что после обновления элементы будут запрошены заново из сервиса.")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+
+		t.Run("After Set Schema", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl1, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.")
+
+			vl3, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.")
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("SetSchema", mock.Anything, spaceID, envID, colID, mock.Anything).Return(nil).Once()
+			err = svc.SetSchema(ctx, spaceID, envID, colID, &schema.Schema{})
+			require.NoError(t, err)
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "nameUPD"}}, nil).Once()
+
+			v4, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v4, "Ожидает что элемент после обновления схемы был удален из кэша и будет запрошен заново из сервиса.")
+
+			v5, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v4, v5, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl4, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.NotSame(t, vl4[0], vl3[0], "Ожидает что после обновления схемы элементы будут запрошены заново из сервиса.")
+
+			vl5, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов из кеша по Alias окружения..")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по ID окружения.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по Alias окружения.")
+
+			vl1, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.")
+
+			vl3, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.")
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Delete", mock.Anything, spaceID, envID, colID).Return(nil).Once()
+			err = svc.Delete(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(nil, errNotFound).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{}, nil).Once()
+
+			_, err = svc.Get(ctx, spaceID, envID, colID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кэша и получена ошибка от сервиса.")
+
+			vl4, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl4, 0, "Ожидает что элементы были удалены из кэша.")
+
+			col.On("Get", mock.Anything, spaceID, envAlias, colID).Return(nil, errNotFound).Once()
+
+			_, err = svc.Get(ctx, spaceID, envAlias, colID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кэша и получена ошибка от сервиса.")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Twice()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кеша по ID окружения.")
+
+			vl3, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl2, 1)
+			assert.Equal(t, vl2[0], vl3[0], "Ожидается получение объектов из кеша по Alias окружения.")
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("Create", mock.Anything, mock.Anything).Return(&collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"}, nil).Once()
+			_, err = svc.Create(ctx, &collections.Collection{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"})
+			require.NoError(t, err)
+
+			//env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envID, mock.Anything).Return([]*collections.Collection{
+				{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"},
+				{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"},
+			}, nil).Once()
+			col.On("List", mock.Anything, spaceID, envAlias, mock.Anything).Return([]*collections.Collection{
+				{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"},
+				{ID: "colID2", SpaceID: spaceID, EnvID: envID, Name: "name2"},
+			}, nil).Once()
+
+			vl4, err := svc.List(ctx, spaceID, envID, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl4, 2, "Ожидает что элементы были удалены из кэша и получены заново из сервиса.")
+
+			vl5, err := svc.List(ctx, spaceID, envAlias, nil)
+			require.NoError(t, err)
+			assert.Len(t, vl5, 2)
+			assert.Equal(t, vl4[0], vl5[0], "Ожидается получение объектов из кеша по Alias окружения..")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			col := &colsmocks.Collections{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), env)(col)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil)
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			time.Sleep(2 * ttl)
+
+			col.On("Get", mock.Anything, spaceID, envID, colID).Return(&collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "name"}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидает что элемент был удален из кэша и будет запрошен заново из сервиса.")
+
+			env.AssertExpectations(t)
+			col.AssertExpectations(t)
+		})
+	})
+
+}
diff --git a/pkg/collections/middleware/error_logging_middleware.go b/pkg/collections/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..d1be7f66c8564cc162d93a926652c63d0a670e1d
--- /dev/null
+++ b/pkg/collections/middleware/error_logging_middleware.go
@@ -0,0 +1,101 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements collections.Collections that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   collections.Collections
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the collections.Collections with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next collections.Collections) collections.Collections {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, collection)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, envId, collectionId)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, envId, collectionId, options...)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, spaceId, envId, filter)
+}
+
+func (m *errorLoggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.SetSchema(ctx, spaceId, envId, collectionId, schema)
+}
+
+func (m *errorLoggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.SetState(ctx, spaceId, envId, collectionId, state)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, coll)
+}
diff --git a/pkg/collections/middleware/logging_middleware.go b/pkg/collections/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..dd43cb9ffd3e662241a1bb08e7174cfd1c5677df
--- /dev/null
+++ b/pkg/collections/middleware/logging_middleware.go
@@ -0,0 +1,296 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements collections.Collections that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   collections.Collections
+}
+
+// LoggingMiddleware instruments an implementation of the collections.Collections with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next collections.Collections) collections.Collections {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":        ctx,
+		"collection": collection} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, collection)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, envId, collectionId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	collection, err = m.next.Get(ctx, spaceId, envId, collectionId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"collection": collection,
+		"err":        err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return collection, err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId,
+		"filter":  filter} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	collections, err = m.next.List(ctx, spaceId, envId, filter)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"collections": collections,
+		"err":         err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return collections, err
+}
+
+func (m *loggingMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"schema":       schema} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetSchema.Request", fields...)
+
+	err = m.next.SetSchema(ctx, spaceId, envId, collectionId, schema)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetSchema.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"state":        state} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetState.Request", fields...)
+
+	err = m.next.SetState(ctx, spaceId, envId, collectionId, state)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetState.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"coll": coll} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, coll)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/collections/middleware/middleware.go b/pkg/collections/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..911368f0f1df019e0e9b4b22afac9863cd4e523b
--- /dev/null
+++ b/pkg/collections/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"go.uber.org/zap"
+)
+
+type Middleware func(collections.Collections) collections.Collections
+
+func WithLog(s collections.Collections, logger *zap.Logger, log_access bool) collections.Collections {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Collections")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/collections/middleware/recovering_middleware.go b/pkg/collections/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb61326fd05df2473219bccacea776246a68376e
--- /dev/null
+++ b/pkg/collections/middleware/recovering_middleware.go
@@ -0,0 +1,116 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/collections -i Collections -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements collections.Collections that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   collections.Collections
+}
+
+// RecoveringMiddleware instruments an implementation of the collections.Collections with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next collections.Collections) collections.Collections {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, collection *collections.Collection) (created *collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, collection)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, envId, collectionId)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, options ...*collections.GetOptions) (collection *collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, envId, collectionId, options...)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, spaceId string, envId string, filter *collections.Filter) (collections []*collections.Collection, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, spaceId, envId, filter)
+}
+
+func (m *recoveringMiddleware) SetSchema(ctx context.Context, spaceId string, envId string, collectionId string, schema *schema.Schema) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.SetSchema(ctx, spaceId, envId, collectionId, schema)
+}
+
+func (m *recoveringMiddleware) SetState(ctx context.Context, spaceId string, envId string, collectionId string, state *collections.StateInfo) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.SetState(ctx, spaceId, envId, collectionId, state)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, coll *collections.Collection) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, coll)
+}
diff --git a/pkg/environments/middleware/caching_middleware.go b/pkg/environments/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..18b594bc3a193d962d63ef3d5eae6217408d61bd
--- /dev/null
+++ b/pkg/environments/middleware/caching_middleware.go
@@ -0,0 +1,167 @@
+package service
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/environments"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Environments) service.Environments {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Environments
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, env *service.Environment) (environment *service.Environment, err error) {
+
+	environment, err = m.next.Create(ctx, env)
+	if err == nil {
+		m.cache.Remove(environment.SpaceID)
+	}
+	return environment, err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId string, envId string) (environment *service.Environment, err error) {
+
+	value, e := m.cache.Get(makeKey(spaceId, envId))
+	if e == nil {
+		return value.(*service.Environment), err
+	}
+	environment, err = m.next.Get(ctx, spaceId, envId)
+	if err == nil {
+		m.cache.Set(makeKey(spaceId, environment.ID), environment)
+		for _, a := range environment.Aliases {
+			m.cache.Set(makeKey(spaceId, a), environment)
+		}
+	}
+	return environment, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, spaceId string) (environments []*service.Environment, err error) {
+
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.([]*service.Environment), err
+	}
+	environments, err = m.next.List(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, environments)
+	}
+	return environments, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, env *service.Environment) (err error) {
+
+	err = m.next.Update(ctx, env)
+	if err == nil {
+		value, e := m.cache.Get(makeKey(env.SpaceID, env.ID))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+		m.cache.Remove(env.SpaceID)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) {
+
+	err = m.next.Delete(ctx, spaceId, envId)
+	if err == nil {
+		value, e := m.cache.Get(makeKey(spaceId, envId))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+
+	err = m.next.SetAlias(ctx, spaceId, envId, alias)
+	if err == nil {
+		value, e := m.cache.Get(makeKey(spaceId, alias))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+
+		value, e = m.cache.Get(makeKey(spaceId, envId))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+
+	err = m.next.RemoveAlias(ctx, spaceId, envId, alias)
+	if err == nil {
+		m.cache.Remove(spaceId)
+		value, e := m.cache.Get(makeKey(spaceId, alias))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+
+		value, e = m.cache.Get(makeKey(spaceId, envId))
+		if e == nil {
+			env := value.(*service.Environment)
+			m.cache.Remove(makeKey(env.SpaceID, env.ID))
+			for _, a := range env.Aliases {
+				m.cache.Remove(makeKey(env.SpaceID, a))
+			}
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Migrate(ctx context.Context, spaceId, envId string, options ...*service.MigrateOptions) (err error) {
+	err = m.next.Migrate(ctx, spaceId, envId, options...)
+
+	// значение из кэша удалить вне зависимости от наличия ошибки, поскольку состояние окружения могло измениться
+	value, e := m.cache.Get(makeKey(spaceId, envId))
+	if e == nil {
+		env := value.(*service.Environment)
+		m.cache.Remove(makeKey(env.SpaceID, env.ID))
+		for _, a := range env.Aliases {
+			m.cache.Remove(makeKey(env.SpaceID, a))
+		}
+	}
+	m.cache.Remove(spaceId)
+	return err
+}
diff --git a/pkg/environments/middleware/caching_middleware_test.go b/pkg/environments/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f5bfa7a9b5d207ed0c00d60cd126950a6da5aba0
--- /dev/null
+++ b/pkg/environments/middleware/caching_middleware_test.go
@@ -0,0 +1,387 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	mocksenvironments "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestEnvironmentsCache(t *testing.T) {
+
+	const (
+		envID    = "envID"
+		spaceID  = "spaceID"
+		envAlias = "envAlias"
+		size     = 5
+		ttl      = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		envs := &mocksenvironments.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+		envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша при повторном запросе по ID.")
+
+		v3, err := svc.Get(ctx, spaceID, envAlias)
+		require.NoError(t, err)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по alias окружения.")
+
+		envs.AssertExpectations(t)
+	})
+
+	t.Run("Get from cache(by Alias)", func(t *testing.T) {
+		envs := &mocksenvironments.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+		envs.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envAlias)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envAlias)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша по alias.")
+
+		v3, err := svc.Get(ctx, spaceID, envID)
+		require.NoError(t, err)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по ID окружения.")
+
+		envs.AssertExpectations(t)
+	})
+
+	t.Run("List from cache", func(t *testing.T) {
+		envs := &mocksenvironments.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+		envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}}, nil).Once()
+
+		vl1, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+
+		vl2, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+		envs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After SetAlias", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}}, nil).Once()
+			envs.On("SetAlias", mock.Anything, spaceID, envID, envAlias).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			err = svc.SetAlias(ctx, spaceID, envID, envAlias)
+			require.NoError(t, err)
+
+			envs.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}}, nil).Once()
+
+			v4, err := svc.Get(ctx, spaceID, envAlias)
+			require.NoError(t, err)
+			assert.Contains(t, v4.Aliases, envAlias, "Ожидает что элемент будет запрошен из сервиса по Alias.")
+
+			v5, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v4, v5, "Ожидается получение объекта из кэша по ID.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты будут удалены из кэша и запрошены из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After RemoveAlias", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2", envAlias}}}, nil).Once()
+			envs.On("RemoveAlias", mock.Anything, spaceID, envID, envAlias).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша по ID.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по Alias.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			err = svc.RemoveAlias(ctx, spaceID, envID, envAlias)
+			require.NoError(t, err)
+
+			envs.On("Get", mock.Anything, spaceID, envAlias).Return(nil, errNotFound).Once()
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{"envID2"}}}, nil).Once()
+
+			_, err = svc.Get(ctx, spaceID, envAlias)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кеша и сервис вернул ошибку на запрос по несуществующему Alias.")
+
+			v4, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v4, "Ожидает что элемент был удален из кеша и получен из сервиса по ID.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты будут удалены из кэша и запрошены из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After Update", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once()
+			envs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по Alias.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			err = svc.Update(ctx, &environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}})
+			require.NoError(t, err)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}}, nil).Once()
+
+			_, err = svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v4, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v4, "Ожидает что элемент был удален из кэша и будет запрошен заново из сервиса.")
+
+			v5, err := svc.Get(ctx, spaceID, envAlias)
+			require.NoError(t, err)
+			assert.Same(t, v4, v5, "Ожидается получение объекта из кэша по Alias после обновления объекта и получения по ID.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты будут удалены из кэша и запрошены из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After Update(List)", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once()
+			envs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			err = svc.Update(ctx, &environments.Environment{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}})
+			require.NoError(t, err)
+
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "EnvironmentUPD", Aliases: []string{envAlias}}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидает что объекты будут удалены из кэша и запрошены из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}, nil).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment", Aliases: []string{envAlias}}}, nil).Once()
+			envs.On("Delete", mock.Anything, spaceID, envID).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кэша по Alias.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			err = svc.Delete(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(nil, errNotFound).Once()
+			envs.On("Get", mock.Anything, spaceID, envAlias).Return(nil, errNotFound).Once()
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{}, nil).Once()
+
+			_, err = svc.Get(ctx, spaceID, envID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидает что элемент был удален из кэша по ID и получена ошибка от сервиса.")
+
+			_, err = svc.Get(ctx, spaceID, envAlias)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидает что элемент был удален  из кэша по Alias и получена ошибка от сервиса.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 0, "Ожидает что объекты будут удалены из кэша и запрошены из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается получение объектов из кэша.")
+
+			envs.On("Create", mock.Anything, mock.Anything).Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once()
+			_, err = svc.Create(ctx, &environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"})
+			require.NoError(t, err)
+
+			envs.On("List", mock.Anything, spaceID).Return([]*environments.Environment{{ID: envID, SpaceID: spaceID, Description: "Environment"}, {ID: "envID2", SpaceID: spaceID, Description: "Environment2"}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 2, "Ожидает что объекты были удалены из кэша и запрошены заново из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After size exceeded", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(1, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, "envID2").Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once()
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once()
+			envs.On("Get", mock.Anything, spaceID, "envID2").Return(&environments.Environment{ID: "envID2", SpaceID: spaceID, Description: "Environment2"}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, "envID2")
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, "envID2")
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			_, err = svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v5, err := svc.Get(ctx, spaceID, "envID2")
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v5, "Ожидает что объект был удален из кэша и будет запрошен заново из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			envs := &mocksenvironments.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(envs)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once()
+			v1, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+
+			envs.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Description: "Environment"}, nil).Once()
+			v3, err := svc.Get(ctx, spaceID, envID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидает что объект был удален из кэша и будет запрошен заново из сервиса.")
+
+			envs.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/environments/middleware/error_logging_middleware.go b/pkg/environments/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..6d6cb544f129f2925d33be91f6666dcf8ebb8936
--- /dev/null
+++ b/pkg/environments/middleware/error_logging_middleware.go
@@ -0,0 +1,110 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements environments.Environments that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   environments.Environments
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the environments.Environments with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next environments.Environments) environments.Environments {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, env)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, envId)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, envId)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Migrate(ctx, spaceId, envId, options...)
+}
+
+func (m *errorLoggingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.RemoveAlias(ctx, spaceId, envId, alias)
+}
+
+func (m *errorLoggingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.SetAlias(ctx, spaceId, envId, alias)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, env)
+}
diff --git a/pkg/environments/middleware/logging_middleware.go b/pkg/environments/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..26f4eb279126e6a008db8998b83012c8a316ca2f
--- /dev/null
+++ b/pkg/environments/middleware/logging_middleware.go
@@ -0,0 +1,325 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements environments.Environments that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   environments.Environments
+}
+
+// LoggingMiddleware instruments an implementation of the environments.Environments with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next environments.Environments) environments.Environments {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx": ctx,
+		"env": env} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, env)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, envId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	env, err = m.next.Get(ctx, spaceId, envId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"env": env,
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return env, err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	envs, err = m.next.List(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"envs": envs,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return envs, err
+}
+
+func (m *loggingMiddleware) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Migrate.Request", fields...)
+
+	err = m.next.Migrate(ctx, spaceId, envId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Migrate.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId,
+		"alias":   alias} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("RemoveAlias.Request", fields...)
+
+	err = m.next.RemoveAlias(ctx, spaceId, envId, alias)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("RemoveAlias.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"envId":   envId,
+		"alias":   alias} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetAlias.Request", fields...)
+
+	err = m.next.SetAlias(ctx, spaceId, envId, alias)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("SetAlias.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx": ctx,
+		"env": env} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, env)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/environments/middleware/middleware.go b/pkg/environments/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..b5e29a99db170df7d2cc5003b2d9a206a423fef2
--- /dev/null
+++ b/pkg/environments/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"go.uber.org/zap"
+)
+
+type Middleware func(environments.Environments) environments.Environments
+
+func WithLog(s environments.Environments, logger *zap.Logger, log_access bool) environments.Environments {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Environments")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/environments/middleware/recovering_middleware.go b/pkg/environments/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..bf4bec7d46fa8b3e963c65330c6883c9c7fb0c20
--- /dev/null
+++ b/pkg/environments/middleware/recovering_middleware.go
@@ -0,0 +1,127 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/environments -i Environments -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements environments.Environments that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   environments.Environments
+}
+
+// RecoveringMiddleware instruments an implementation of the environments.Environments with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next environments.Environments) environments.Environments {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, env *environments.Environment) (created *environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, env)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, envId)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string) (env *environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, envId)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (envs []*environments.Environment, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) Migrate(ctx context.Context, spaceId string, envId string, options ...*environments.MigrateOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Migrate(ctx, spaceId, envId, options...)
+}
+
+func (m *recoveringMiddleware) RemoveAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.RemoveAlias(ctx, spaceId, envId, alias)
+}
+
+func (m *recoveringMiddleware) SetAlias(ctx context.Context, spaceId string, envId string, alias string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.SetAlias(ctx, spaceId, envId, alias)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, env *environments.Environment) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, env)
+}
diff --git a/pkg/invitations/middleware/caching_middleware.go b/pkg/invitations/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..97a1bcb3367fa0cf0ab0f1bc7c638191cb72911c
--- /dev/null
+++ b/pkg/invitations/middleware/caching_middleware.go
@@ -0,0 +1,62 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/invitations"
+	services "git.perx.ru/perxis/perxis-go/pkg/options"
+)
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Invitations) service.Invitations {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Invitations
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, invitation *service.Invitation) (inv *service.Invitation, err error) {
+	return m.next.Create(ctx, invitation)
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, invitationId string) (inv *service.Invitation, err error) {
+
+	value, e := m.cache.Get(invitationId)
+	if e == nil {
+		return value.(*service.Invitation), err
+	}
+	inv, err = m.next.Get(ctx, invitationId)
+	if err == nil {
+		m.cache.Set(invitationId, inv)
+	}
+	return inv, err
+}
+
+func (m cachingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) {
+
+	err = m.next.Accept(ctx, invitationId, userId)
+	if err == nil {
+		m.cache.Remove(invitationId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, opts *services.FindOptions) (invitations []*service.Invitation, total int, err error) {
+	return m.next.Find(ctx, filter, opts)
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, invitationId string) (err error) {
+
+	err = m.next.Delete(ctx, invitationId)
+	if err == nil {
+		m.cache.Remove(invitationId)
+	}
+	return err
+}
diff --git a/pkg/invitations/middleware/caching_middleware_test.go b/pkg/invitations/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b635ee0cd09b28245a8f29c67d7902a08392eba7
--- /dev/null
+++ b/pkg/invitations/middleware/caching_middleware_test.go
@@ -0,0 +1,129 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	invmocks "git.perx.ru/perxis/perxis-go/pkg/invitations/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestLocalesCache(t *testing.T) {
+
+	const (
+		orgID = "orgID"
+		email = "123@321.ru"
+		invID = "invID"
+		usrID = "usrID"
+		size  = 5
+		ttl   = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from Cache", func(t *testing.T) {
+		inv := &invmocks.Invitations{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(inv)
+
+		inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once()
+
+		v1, err := svc.Get(ctx, invID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, invID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.")
+
+		inv.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("Get from Accept", func(t *testing.T) {
+			inv := &invmocks.Invitations{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(inv)
+
+			inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once()
+
+			v1, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.")
+
+			inv.On("Accept", mock.Anything, invID, usrID).Return(nil).Once()
+			inv.On("Get", mock.Anything, invID).Return(nil, errNotFound).Once()
+
+			err = svc.Accept(ctx, invID, usrID)
+			require.NoError(t, err)
+
+			_, err = svc.Get(ctx, invID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после подтверждения объект будет удален из кэша и получена ошибка от сервиса.")
+
+			inv.AssertExpectations(t)
+		})
+
+		t.Run("Get from Delete", func(t *testing.T) {
+			inv := &invmocks.Invitations{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(inv)
+
+			inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once()
+
+			v1, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.")
+
+			inv.On("Delete", mock.Anything, invID).Return(nil).Once()
+			inv.On("Get", mock.Anything, invID).Return(nil, errNotFound).Once()
+
+			err = svc.Delete(ctx, invID)
+			require.NoError(t, err)
+
+			_, err = svc.Get(ctx, invID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаления кэша будет очищен и получена ошибка от сервиса.")
+
+			inv.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			inv := &invmocks.Invitations{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(inv)
+
+			inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once()
+
+			v1, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается что при повторном запросе объект будет получен из кэша.")
+
+			time.Sleep(2 * ttl)
+
+			inv.On("Get", mock.Anything, invID).Return(&invitations.Invitation{ID: invID, Email: email, OrgID: orgID}, nil).Once()
+
+			v3, err := svc.Get(ctx, invID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что при истечении ttl кеш будет очищен..")
+
+			inv.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/invitations/middleware/error_logging_middleware.go b/pkg/invitations/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..39c823ae6fdf7fb7cb5c181c42099ca16c1a2bad
--- /dev/null
+++ b/pkg/invitations/middleware/error_logging_middleware.go
@@ -0,0 +1,81 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements invitations.Invitations that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   invitations.Invitations
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the invitations.Invitations with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next invitations.Invitations) invitations.Invitations {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Accept(ctx, invitationId, userId)
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, invitation)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, invitationId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, invitationId)
+}
+
+func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Find(ctx, filter, opts)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, invitationId)
+}
diff --git a/pkg/invitations/middleware/logging_middleware.go b/pkg/invitations/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f1ceb9959497794cc4bc7a2d6a963a949d9b1a3
--- /dev/null
+++ b/pkg/invitations/middleware/logging_middleware.go
@@ -0,0 +1,216 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements invitations.Invitations that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   invitations.Invitations
+}
+
+// LoggingMiddleware instruments an implementation of the invitations.Invitations with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next invitations.Invitations) invitations.Invitations {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"invitationId": invitationId,
+		"userId":       userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Accept.Request", fields...)
+
+	err = m.next.Accept(ctx, invitationId, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Accept.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":        ctx,
+		"invitation": invitation} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, invitation)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, invitationId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"invitationId": invitationId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, invitationId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"filter": filter,
+		"opts":   opts} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Request", fields...)
+
+	invitations, total, err = m.next.Find(ctx, filter, opts)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"invitations": invitations,
+		"total":       total,
+		"err":         err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Response", fields...)
+
+	return invitations, total, err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"invitationId": invitationId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	invitation, err = m.next.Get(ctx, invitationId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"invitation": invitation,
+		"err":        err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return invitation, err
+}
diff --git a/pkg/invitations/middleware/middleware.go b/pkg/invitations/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1c054d8ae96ab5d45f1dd83af9bb440b3d429817
--- /dev/null
+++ b/pkg/invitations/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"go.uber.org/zap"
+)
+
+type Middleware func(invitations.Invitations) invitations.Invitations
+
+func WithLog(s invitations.Invitations, logger *zap.Logger, log_access bool) invitations.Invitations {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Invitations")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/invitations/middleware/recovering_middleware.go b/pkg/invitations/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..195933ecce0d9766d8a92bbe1e52df6dc4d7064e
--- /dev/null
+++ b/pkg/invitations/middleware/recovering_middleware.go
@@ -0,0 +1,92 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/invitations -i Invitations -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/invitations"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements invitations.Invitations that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   invitations.Invitations
+}
+
+// RecoveringMiddleware instruments an implementation of the invitations.Invitations with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next invitations.Invitations) invitations.Invitations {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Accept(ctx context.Context, invitationId string, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Accept(ctx, invitationId, userId)
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, invitation *invitations.Invitation) (created *invitations.Invitation, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, invitation)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, invitationId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, invitationId)
+}
+
+func (m *recoveringMiddleware) Find(ctx context.Context, filter *invitations.Filter, opts *options.FindOptions) (invitations []*invitations.Invitation, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Find(ctx, filter, opts)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, invitationId string) (invitation *invitations.Invitation, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, invitationId)
+}
diff --git a/pkg/items/middleware/caching_middleware.go b/pkg/items/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..0455cb276c5ccae86aeb27619fe55369a735550a
--- /dev/null
+++ b/pkg/items/middleware/caching_middleware.go
@@ -0,0 +1,176 @@
+package service
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	envService "git.perx.ru/perxis/perxis-go/pkg/environments"
+	service "git.perx.ru/perxis/perxis-go/pkg/items"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache, cachePublished *cache.Cache, envs envService.Environments) Middleware {
+	return func(next service.Items) service.Items {
+		return &cachingMiddleware{
+			cache:          cache,
+			cachePublished: cachePublished,
+			Items:          next,
+			envs:           envs,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache          *cache.Cache
+	cachePublished *cache.Cache
+	envs           envService.Environments
+	service.Items
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.GetOptions) (itm *service.Item, err error) {
+
+	value, e := m.cache.Get(makeKey(spaceId, envId, collectionId, itemId))
+	if e == nil {
+		return value.(*service.Item), err
+	}
+	itm, err = m.Items.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, itm.SpaceID, itm.EnvID)
+		if err != nil {
+			return nil, err
+		}
+		m.cache.Set(makeKey(itm.SpaceID, env.ID, itm.CollectionID, itm.ID), itm)
+		for _, al := range env.Aliases {
+			m.cache.Set(makeKey(itm.SpaceID, al, itm.CollectionID, itm.ID), itm)
+		}
+	}
+	return itm, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, item *service.Item, options ...*service.UpdateOptions) (err error) {
+
+	err = m.Items.Update(ctx, item, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+			m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.DeleteOptions) (err error) {
+
+	err = m.Items.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, spaceId, envId)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(spaceId, env.ID, collectionId, itemId))
+		m.cachePublished.Remove(makeKey(spaceId, env.ID, collectionId, itemId))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(spaceId, al, collectionId, itemId))
+			m.cachePublished.Remove(makeKey(spaceId, al, collectionId, itemId))
+		}
+
+	}
+	return err
+}
+
+func (m cachingMiddleware) Publish(ctx context.Context, item *service.Item, options ...*service.PublishOptions) (err error) {
+
+	err = m.Items.Publish(ctx, item, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+			m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Unpublish(ctx context.Context, item *service.Item, options ...*service.UnpublishOptions) (err error) {
+
+	err = m.Items.Unpublish(ctx, item, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+			m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) GetPublished(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*service.GetPublishedOptions) (itm *service.Item, err error) {
+
+	opts := service.MergeGetPublishedOptions(options...)
+
+	val, e := m.cachePublished.Get(makeKey(spaceId, envId, collectionId, itemId))
+	if e == nil {
+		value := val.(map[string]*service.Item)
+		if i, ok := value[opts.LocaleID]; ok {
+			return i, nil
+		}
+	}
+
+	itm, err = m.Items.GetPublished(ctx, spaceId, envId, collectionId, itemId, opts)
+
+	if err == nil {
+		env, err := m.envs.Get(ctx, itm.SpaceID, itm.EnvID)
+		if err != nil {
+			return nil, err
+		}
+		var value = make(map[string]*service.Item)
+		if val != nil {
+			value = val.(map[string]*service.Item)
+		}
+		value[opts.LocaleID] = itm
+		m.cachePublished.Set(makeKey(itm.SpaceID, env.ID, itm.CollectionID, itm.ID), value)
+		for _, al := range env.Aliases {
+			m.cachePublished.Set(makeKey(itm.SpaceID, al, itm.CollectionID, itm.ID), value)
+		}
+	}
+
+	return itm, err
+}
+
+func (m cachingMiddleware) Archive(ctx context.Context, item *service.Item, options ...*service.ArchiveOptions) (err error) {
+
+	err = m.Items.Archive(ctx, item, options...)
+	if err == nil {
+		env, err := m.envs.Get(ctx, item.SpaceID, item.EnvID)
+		if err != nil {
+			return err
+		}
+		m.cache.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		m.cachePublished.Remove(makeKey(item.SpaceID, env.ID, item.CollectionID, item.ID))
+		for _, al := range env.Aliases {
+			m.cache.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+			m.cachePublished.Remove(makeKey(item.SpaceID, al, item.CollectionID, item.ID))
+		}
+	}
+	return err
+}
diff --git a/pkg/items/middleware/caching_middleware_test.go b/pkg/items/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..9e02b49beb80507ef7fbf25d3c67d5c8a022ce1b
--- /dev/null
+++ b/pkg/items/middleware/caching_middleware_test.go
@@ -0,0 +1,685 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/environments"
+	envmocks "git.perx.ru/perxis/perxis-go/pkg/environments/mocks"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	itmsmocks "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestItemsCache(t *testing.T) {
+
+	const (
+		colID    = "colID"
+		spaceID  = "spaceID"
+		envID    = "envID"
+		envAlias = "envAlias"
+		itemID   = "itemID"
+		locID    = "locID"
+		size     = 5
+		ttl      = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		itms := &itmsmocks.Items{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша, при повторном запросе.")
+
+		v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по alias окружения.")
+		require.NoError(t, err)
+
+		env.AssertExpectations(t)
+		itms.AssertExpectations(t)
+	})
+
+	t.Run("Get from cache(by Alias)", func(t *testing.T) {
+		itms := &itmsmocks.Items{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша, при повторном запросе.")
+
+		v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+		assert.Same(t, v3, v2, "Ожидается получение объекта из кеша, при запросе того же объекта по ID окружения.")
+		require.NoError(t, err)
+
+		env.AssertExpectations(t)
+		itms.AssertExpectations(t)
+	})
+
+	t.Run("GetPublished from cache", func(t *testing.T) {
+		itms := &itmsmocks.Items{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+		v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+
+		v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+		v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кеша, при запросе того же объекта по alias окружения.")
+
+		env.AssertExpectations(t)
+		itms.AssertExpectations(t)
+	})
+
+	t.Run("GetPublished from cache(by Alias)", func(t *testing.T) {
+		itms := &itmsmocks.Items{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+		itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+		v1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+
+		v2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+		v3, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кеша, при запросе того же объекта по ID окружения.")
+
+		env.AssertExpectations(t)
+		itms.AssertExpectations(t)
+	})
+
+	t.Run("GetPublished from cache (with different locales)", func(t *testing.T) {
+		const (
+			loc1 = "loc1"
+			loc2 = "loc2"
+		)
+
+		itms := &itmsmocks.Items{}
+		env := &envmocks.Environments{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+		env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Twice()
+		itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+		itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+		v1loc1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1})
+		require.NoError(t, err, "Ожидается получение объекта из сервиса и добавление его в кеш с loc1.")
+
+		v1loc2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2})
+		require.NoError(t, err, "Ожидается получение объекта из сервиса и добавление его в кеш с loc2 вместе с loc1.")
+
+		v2loc1, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1})
+		require.NoError(t, err)
+		assert.Same(t, v1loc1, v2loc1, "Ожидается получение объекта c локализацией loc1 из кеша.")
+
+		v2loc2, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2})
+		require.NoError(t, err)
+		assert.Same(t, v1loc2, v2loc2, "Ожидается получение объекта c локализацией loc2 из кеша.")
+
+		v3loc1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: loc1})
+		require.NoError(t, err)
+		assert.Same(t, v2loc1, v3loc1, "Ожидается получение объекта c локализацией loc1 из кеша, при запросе того же объекта по ID окружения.")
+
+		v3loc2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: loc2})
+		require.NoError(t, err)
+		assert.Same(t, v2loc2, v3loc2, "Ожидается получение объекта c локализацией loc2 из кеша, при запросе того же объекта по ID окружения.")
+
+		env.AssertExpectations(t)
+		itms.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Update(Get)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d1"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после обновления и получение его заново из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Archive(Get)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			itms.On("Archive", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Archive(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateArchived}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после архивации и получение из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Publish(Get)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			itms.On("Publish", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			err = svc.Publish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft})
+			require.NoError(t, err)
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после публикации и получение заново из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша при повторном запросе.")
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по alias окружения.")
+
+			itms.On("Delete", mock.Anything, spaceID, envID, colID, itemID).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Delete(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(nil, errNotFound).Once()
+			itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(nil, errNotFound).Once()
+			_, err = svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление из кэша после удаления объекта и получение ошибки от сервиса.")
+
+			_, err = svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление из кэша после удаления объекта и получение ошибки от сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Unpublish(Get)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished})
+			require.NoError(t, err)
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после снятия с публикации и получение заново из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Publish(Get by Alias)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по alias окружения.")
+
+			itms.On("Publish", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Publish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после публикации и получение из сервиса по alias окружения.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Update(Get by Alias)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d1"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по alias окружения.")
+
+			itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}})
+			require.NoError(t, err)
+
+			itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, Data: map[string]interface{}{"f1": "d2"}}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша при обновлении и получение из сервиса по alias окружения.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Unpublish(Get by Alias)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша по alias окружения.")
+
+			itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished})
+			require.NoError(t, err)
+
+			itms.On("Get", mock.Anything, spaceID, envAlias, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envAlias, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кэша после снятия с публикации и получение из сервиса по alias окружения.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Update(GetPublished)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished, Data: map[string]interface{}{"f1": "d1"}}, nil).Once()
+
+			v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+
+			v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по о alias окружения.")
+
+			itms.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Update(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished, Data: map[string]interface{}{"f1": "d2"}})
+			require.NoError(t, err)
+
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+
+			_, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по ID окружения после его обновления и получение ошибки от сервиса.")
+
+			_, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по alias окружения после его обновления и получение ошибки от сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Archive(GetPublished)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+
+			v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по о alias окружения.")
+
+			itms.On("Archive", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Archive(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft})
+			require.NoError(t, err)
+
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+
+			_, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по ID окружения после его архивации и получение ошибки от сервиса.")
+
+			_, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по alias окружения после его архивации и получение ошибки от сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Delete(GetPublished)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+
+			v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по о alias окружения.")
+
+			itms.On("Delete", mock.Anything, spaceID, envID, colID, itemID).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Delete(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+
+			_, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша после удаления из хранилища и получение ошибки от сервиса.")
+
+			_, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается очистка кеша по alias окружения после удаления объекта и получение ошибки от сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Unpublish(GetPublished)", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+
+			v2, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			v3, err := svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v2, v3, "Ожидается получение объекта из кеша по о alias окружения.")
+
+			itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished})
+			require.NoError(t, err)
+
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+
+			_, err = svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по ID окружения после снятия с публикации и получение ошибки от сервиса.")
+
+			_, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша по alias окружения после снятия с публикации и получение ошибки от сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After Unpublish by Alias", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			env.On("Get", mock.Anything, spaceID, envAlias).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envID, colID, itemID, mock.Anything).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StatePublished}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			v3, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+
+			v4, err := svc.GetPublished(ctx, spaceID, envID, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.NoError(t, err)
+			assert.Same(t, v3, v4, "Ожидается получение опубликованного объекта из кеша.")
+
+			itms.On("Unpublish", mock.Anything, mock.Anything).Return(nil).Once()
+			err = svc.Unpublish(ctx, &items.Item{ID: itemID, SpaceID: spaceID, EnvID: envAlias, CollectionID: colID, State: items.StatePublished})
+			require.NoError(t, err)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+			itms.On("GetPublished", mock.Anything, spaceID, envAlias, colID, itemID, mock.Anything).Return(nil, errNotFound).Once()
+
+			v5, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v5, v2, "Ожидается удаление объекта из кэша и получение заново из сервиса.")
+
+			_, err = svc.GetPublished(ctx, spaceID, envAlias, colID, itemID, &items.GetPublishedOptions{LocaleID: locID})
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша и получение ошибки от сервиса из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			itms := &itmsmocks.Items{}
+			env := &envmocks.Environments{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl), cache.NewCache(size, ttl), env)(itms)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кеша.")
+
+			time.Sleep(2 * ttl)
+
+			env.On("Get", mock.Anything, spaceID, envID).Return(&environments.Environment{ID: envID, SpaceID: spaceID, Aliases: []string{envAlias}}, nil).Once()
+			itms.On("Get", mock.Anything, spaceID, envID, colID, itemID).Return(&items.Item{ID: itemID, SpaceID: spaceID, EnvID: envID, CollectionID: colID, State: items.StateDraft}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, envID, colID, itemID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается удаление объекта из кэша и получение из сервиса.")
+
+			env.AssertExpectations(t)
+			itms.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/items/middleware/client_encode_middleware.go b/pkg/items/middleware/client_encode_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..3aaa6b94badd9726e74d1cb3b0abbd02893f5838
--- /dev/null
+++ b/pkg/items/middleware/client_encode_middleware.go
@@ -0,0 +1,317 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+)
+
+// ClientEncodeMiddleware выполняет операции encode/decode для передаваемых данных
+func ClientEncodeMiddleware(colls collections.Collections) Middleware {
+	return func(items items.Items) items.Items {
+		return &encodeDecodeMiddleware{
+			next:  items,
+			colls: colls,
+		}
+
+	}
+}
+
+type encodeDecodeMiddleware struct {
+	next  items.Items
+	colls collections.Collections
+}
+
+func (m *encodeDecodeMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) {
+	coll, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	if item, err = item.Encode(ctx, coll.Schema); err != nil {
+		return
+	}
+
+	itm, sch, err = m.next.Introspect(ctx, item, opts...)
+	if itm != nil && sch != nil {
+		var err error
+		if itm, err = itm.Decode(ctx, sch); err != nil {
+			return nil, nil, err
+		}
+	}
+	return itm, sch, err
+
+}
+
+func (m *encodeDecodeMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) {
+
+	var col *collections.Collection
+
+	if item != nil && (item.Data != nil || item.Translations != nil) {
+
+		col, err = m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+		if err != nil {
+			return nil, err
+		}
+
+		if item, err = item.Encode(ctx, col.Schema); err != nil {
+			return nil, err
+		}
+	}
+
+	res, err := m.next.Create(ctx, item, opts...)
+	if err == nil && (res.Data != nil || res.Translations != nil) {
+
+		if col == nil {
+			col, err = m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		res, err = res.Decode(ctx, col.Schema)
+	}
+
+	return res, err
+}
+
+func (m *encodeDecodeMiddleware) Update(ctx context.Context, upd *items.Item, options ...*items.UpdateOptions) (err error) {
+	var col *collections.Collection
+	if upd != nil && (upd.Data != nil || upd.Translations != nil) {
+		col, err = m.colls.Get(ctx, upd.SpaceID, upd.EnvID, upd.CollectionID)
+		if err != nil {
+			return err
+		}
+		if upd, err = upd.Encode(ctx, col.Schema); err != nil {
+			return err
+		}
+	}
+	return m.next.Update(ctx, upd, options...)
+}
+
+func (m *encodeDecodeMiddleware) Find(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) {
+	items, total, err = m.next.Find(ctx, spaceId, envId, collectionId, filter, options...)
+	if err == nil && total > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, 0, err
+		}
+		for i, itm := range items {
+			itm, err = itm.Decode(ctx, col.Schema)
+			if err != nil {
+				return nil, 0, err
+			}
+
+			items[i] = itm
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) Get(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.GetOptions) (item *items.Item, err error) {
+	item, err = m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+	if err == nil && item != nil {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, err
+		}
+		item, err = item.Decode(ctx, col.Schema)
+		if err != nil {
+			return nil, err
+
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) Publish(ctx context.Context, item *items.Item, opts ...*items.PublishOptions) (err error) {
+	if item != nil && (item.Data != nil || item.Translations != nil) {
+		col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+		if err != nil {
+			return err
+		}
+
+		if item, err = item.Encode(ctx, col.Schema); err != nil {
+			return err
+		}
+	}
+
+	return m.next.Publish(ctx, item, opts...)
+}
+
+func (m *encodeDecodeMiddleware) Unpublish(ctx context.Context, item *items.Item, opts ...*items.UnpublishOptions) (err error) {
+	if item != nil && (item.Data != nil || item.Translations != nil) {
+		col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+		if err != nil {
+			return err
+		}
+
+		if item, err = item.Encode(ctx, col.Schema); err != nil {
+			return err
+		}
+	}
+
+	return m.next.Unpublish(ctx, item, opts...)
+}
+
+func (m *encodeDecodeMiddleware) GetPublished(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	item, err = m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...)
+	if err == nil && item != nil {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, err
+		}
+		item, err = item.Decode(ctx, col.Schema)
+		if err != nil {
+			return nil, err
+
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) FindPublished(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	items, total, err = m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...)
+	if err == nil && total > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, 0, err
+		}
+		for i, itm := range items {
+			itm, err = itm.Decode(ctx, col.Schema)
+			if err != nil {
+				return nil, 0, err
+			}
+
+			items[i] = itm
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) GetRevision(ctx context.Context, spaceId, envId, collectionId, itemId, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) {
+	item, err = m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...)
+	if err == nil && item != nil {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, err
+		}
+		item, err = item.Decode(ctx, col.Schema)
+		if err != nil {
+			return nil, err
+
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) ListRevisions(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) {
+	items, err = m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...)
+	if err == nil && len(items) > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, err
+		}
+		for i, itm := range items {
+			itm, err = itm.Decode(ctx, col.Schema)
+			if err != nil {
+				return nil, err
+			}
+
+			items[i] = itm
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) FindArchived(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) {
+	items, total, err = m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...)
+	if err == nil && total > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, 0, err
+		}
+		for i, itm := range items {
+			itm, err = itm.Decode(ctx, col.Schema)
+			if err != nil {
+				return nil, 0, err
+			}
+
+			items[i] = itm
+		}
+	}
+	return
+}
+
+func (m *encodeDecodeMiddleware) Archive(ctx context.Context, item *items.Item, opts ...*items.ArchiveOptions) (err error) {
+	if item != nil && (item.Data != nil || item.Translations != nil) {
+		col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+		if err != nil {
+			return err
+		}
+
+		if item, err = item.Encode(ctx, col.Schema); err != nil {
+			return err
+		}
+	}
+
+	return m.next.Archive(ctx, item, opts...)
+}
+
+func (m *encodeDecodeMiddleware) Unarchive(ctx context.Context, item *items.Item, opts ...*items.UnarchiveOptions) (err error) {
+	if item != nil && (item.Data != nil || item.Translations != nil) {
+		col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+		if err != nil {
+			return err
+		}
+
+		if item, err = item.Encode(ctx, col.Schema); err != nil {
+			return err
+		}
+	}
+
+	return m.next.Unarchive(ctx, item, opts...)
+}
+
+func (m *encodeDecodeMiddleware) Delete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.DeleteOptions) (err error) {
+	return m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *encodeDecodeMiddleware) Undelete(ctx context.Context, spaceId, envId, collectionId, itemId string, options ...*items.UndeleteOptions) (err error) {
+	return m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *encodeDecodeMiddleware) Aggregate(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) {
+	res, err := m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+	if len(res) > 0 && len(options) > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, errors.Wrap(err, "encode aggregate result")
+		}
+		o := items.MergeAggregateOptions(options...)
+		res, err = items.DecodeAggregateResult(ctx, o.Fields, res, col.Schema)
+		if err != nil {
+			return nil, errors.Wrap(err, "encode aggregate result")
+		}
+	}
+	return res, err
+}
+
+func (m *encodeDecodeMiddleware) AggregatePublished(ctx context.Context, spaceId, envId, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	res, err := m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...)
+	if len(res) > 0 && len(options) > 0 {
+		col, err := m.colls.Get(ctx, spaceId, envId, collectionId)
+		if err != nil {
+			return nil, errors.Wrap(err, "get collection")
+		}
+		o := items.MergeAggregatePublishedOptions(options...)
+		res, err = items.DecodeAggregateResult(ctx, o.Fields, res, col.Schema)
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, err
+}
diff --git a/pkg/items/middleware/error_logging_middleware.go b/pkg/items/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..97967808d150cf951f3fb22b16b2836765fc611f
--- /dev/null
+++ b/pkg/items/middleware/error_logging_middleware.go
@@ -0,0 +1,211 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements items.Items that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   items.Items
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the items.Items with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next items.Items) items.Items {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *errorLoggingMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *errorLoggingMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Archive(ctx, item, options...)
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, item, opts...)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *errorLoggingMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Find(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *errorLoggingMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *errorLoggingMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *errorLoggingMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *errorLoggingMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...)
+}
+
+func (m *errorLoggingMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Introspect(ctx, item, opts...)
+}
+
+func (m *errorLoggingMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *errorLoggingMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Publish(ctx, item, options...)
+}
+
+func (m *errorLoggingMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Unarchive(ctx, item, options...)
+}
+
+func (m *errorLoggingMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *errorLoggingMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Unpublish(ctx, item, options...)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, item, options...)
+}
diff --git a/pkg/items/middleware/logging_middleware.go b/pkg/items/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..102b91874c63655c169e5c101b10490e375833b9
--- /dev/null
+++ b/pkg/items/middleware/logging_middleware.go
@@ -0,0 +1,732 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements items.Items that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   items.Items
+}
+
+// LoggingMiddleware instruments an implementation of the items.Items with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next items.Items) items.Items {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"filter":       filter,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Aggregate.Request", fields...)
+
+	result, err = m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"result": result,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Aggregate.Response", fields...)
+
+	return result, err
+}
+
+func (m *loggingMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"filter":       filter,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("AggregatePublished.Request", fields...)
+
+	result, err = m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"result": result,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("AggregatePublished.Response", fields...)
+
+	return result, err
+}
+
+func (m *loggingMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"item":    item,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Archive.Request", fields...)
+
+	err = m.next.Archive(ctx, item, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Archive.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"item": item,
+		"opts": opts} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, item, opts...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"filter":       filter,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Request", fields...)
+
+	items, total, err = m.next.Find(ctx, spaceId, envId, collectionId, filter, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"items": items,
+		"total": total,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Response", fields...)
+
+	return items, total, err
+}
+
+func (m *loggingMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"filter":       filter,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("FindArchived.Request", fields...)
+
+	items, total, err = m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"items": items,
+		"total": total,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("FindArchived.Response", fields...)
+
+	return items, total, err
+}
+
+func (m *loggingMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"filter":       filter,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("FindPublished.Request", fields...)
+
+	items, total, err = m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"items": items,
+		"total": total,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("FindPublished.Response", fields...)
+
+	return items, total, err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	item, err = m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"item": item,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return item, err
+}
+
+func (m *loggingMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetPublished.Request", fields...)
+
+	item, err = m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"item": item,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetPublished.Response", fields...)
+
+	return item, err
+}
+
+func (m *loggingMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"revisionId":   revisionId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetRevision.Request", fields...)
+
+	item, err = m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"item": item,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetRevision.Response", fields...)
+
+	return item, err
+}
+
+func (m *loggingMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"item": item,
+		"opts": opts} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Introspect.Request", fields...)
+
+	itm, sch, err = m.next.Introspect(ctx, item, opts...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"itm": itm,
+		"sch": sch,
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Introspect.Response", fields...)
+
+	return itm, sch, err
+}
+
+func (m *loggingMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListRevisions.Request", fields...)
+
+	items, err = m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"items": items,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListRevisions.Response", fields...)
+
+	return items, err
+}
+
+func (m *loggingMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"item":    item,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Publish.Request", fields...)
+
+	err = m.next.Publish(ctx, item, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Publish.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"item":    item,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Unarchive.Request", fields...)
+
+	err = m.next.Unarchive(ctx, item, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Unarchive.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":          ctx,
+		"spaceId":      spaceId,
+		"envId":        envId,
+		"collectionId": collectionId,
+		"itemId":       itemId,
+		"options":      options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Undelete.Request", fields...)
+
+	err = m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Undelete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"item":    item,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Unpublish.Request", fields...)
+
+	err = m.next.Unpublish(ctx, item, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Unpublish.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"item":    item,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, item, options...)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/items/middleware/middleware.go b/pkg/items/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..a1090fe5ad072cd42682c1a4fad8504f22136926
--- /dev/null
+++ b/pkg/items/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"go.uber.org/zap"
+)
+
+type Middleware func(items.Items) items.Items
+
+func WithLog(s items.Items, logger *zap.Logger, log_access bool) items.Items {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Items")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/items/middleware/recovering_middleware.go b/pkg/items/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..244fc8a0638fa9ad812afeb3f919907171fb9b1e
--- /dev/null
+++ b/pkg/items/middleware/recovering_middleware.go
@@ -0,0 +1,248 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/items -i Items -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/schema"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements items.Items that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   items.Items
+}
+
+// RecoveringMiddleware instruments an implementation of the items.Items with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next items.Items) items.Items {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Aggregate(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregateOptions) (result map[string]interface{}, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Aggregate(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *recoveringMiddleware) AggregatePublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.AggregatePublishedOptions) (result map[string]interface{}, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.AggregatePublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *recoveringMiddleware) Archive(ctx context.Context, item *items.Item, options ...*items.ArchiveOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Archive(ctx, item, options...)
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, item *items.Item, opts ...*items.CreateOptions) (created *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, item, opts...)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.DeleteOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *recoveringMiddleware) Find(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Find(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *recoveringMiddleware) FindArchived(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindArchivedOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.FindArchived(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *recoveringMiddleware) FindPublished(ctx context.Context, spaceId string, envId string, collectionId string, filter *items.Filter, options ...*items.FindPublishedOptions) (items []*items.Item, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.FindPublished(ctx, spaceId, envId, collectionId, filter, options...)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *recoveringMiddleware) GetPublished(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.GetPublishedOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.GetPublished(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *recoveringMiddleware) GetRevision(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, revisionId string, options ...*items.GetRevisionOptions) (item *items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.GetRevision(ctx, spaceId, envId, collectionId, itemId, revisionId, options...)
+}
+
+func (m *recoveringMiddleware) Introspect(ctx context.Context, item *items.Item, opts ...*items.IntrospectOptions) (itm *items.Item, sch *schema.Schema, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Introspect(ctx, item, opts...)
+}
+
+func (m *recoveringMiddleware) ListRevisions(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.ListRevisionsOptions) (items []*items.Item, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.ListRevisions(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *recoveringMiddleware) Publish(ctx context.Context, item *items.Item, options ...*items.PublishOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Publish(ctx, item, options...)
+}
+
+func (m *recoveringMiddleware) Unarchive(ctx context.Context, item *items.Item, options ...*items.UnarchiveOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Unarchive(ctx, item, options...)
+}
+
+func (m *recoveringMiddleware) Undelete(ctx context.Context, spaceId string, envId string, collectionId string, itemId string, options ...*items.UndeleteOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Undelete(ctx, spaceId, envId, collectionId, itemId, options...)
+}
+
+func (m *recoveringMiddleware) Unpublish(ctx context.Context, item *items.Item, options ...*items.UnpublishOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Unpublish(ctx, item, options...)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, item *items.Item, options ...*items.UpdateOptions) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, item, options...)
+}
diff --git a/pkg/items/pagination.go b/pkg/items/pagination.go
new file mode 100644
index 0000000000000000000000000000000000000000..7f990dc6c6e8d0684553e4039b86580236cc2ef0
--- /dev/null
+++ b/pkg/items/pagination.go
@@ -0,0 +1,137 @@
+package items
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/data"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"google.golang.org/grpc/codes"
+)
+
+type BatchProcessor struct {
+	Content                      *content.Content
+	SpaceID, EnvID, CollectionID string
+	FindOptions                  *FindOptions
+	FindPublishedOptions         *FindPublishedOptions
+	Filter                       *Filter
+
+	pageSize, pageNum int
+	sort              []string
+	processed         int
+}
+
+func (b *BatchProcessor) getBatch(ctx context.Context) ([]*Item, bool, error) {
+	var res []*Item
+	var err error
+	var total int
+
+	if b.FindPublishedOptions != nil {
+		res, total, err = b.Content.Items.FindPublished(
+			ctx,
+			b.SpaceID,
+			b.EnvID,
+			b.CollectionID,
+			b.Filter,
+			&FindPublishedOptions{
+				Regular:     b.FindPublishedOptions.Regular,
+				Hidden:      b.FindPublishedOptions.Hidden,
+				Templates:   b.FindPublishedOptions.Templates,
+				FindOptions: *options.NewFindOptions(b.pageNum, b.pageSize, b.sort...),
+			},
+		)
+	} else {
+		res, total, err = b.Content.Items.Find(
+			ctx,
+			b.SpaceID,
+			b.EnvID,
+			b.CollectionID,
+			b.Filter,
+			&FindOptions{
+				Deleted:     b.FindOptions.Deleted,
+				Regular:     b.FindOptions.Regular,
+				Hidden:      b.FindOptions.Hidden,
+				Templates:   b.FindOptions.Templates,
+				FindOptions: *options.NewFindOptions(b.pageNum, b.pageSize, b.sort...),
+			},
+		)
+	}
+
+	if err == nil {
+		b.processed += len(res)
+		b.pageNum++
+	}
+
+	return res, b.processed != total, err
+}
+
+func (b *BatchProcessor) next(ctx context.Context) (res []*Item, next bool, err error) {
+
+	for {
+		res, next, err = b.getBatch(ctx)
+		if err != nil {
+			if errors.GetStatusCode(err) == codes.ResourceExhausted && b.reducePageSize() {
+				continue
+			}
+
+			return nil, false, err
+		}
+
+		break
+	}
+
+	return res, next, nil
+}
+
+func (b *BatchProcessor) reducePageSize() bool {
+	if b.pageSize == 1 {
+		return false
+	}
+
+	b.pageNum = 2 * b.pageNum
+	b.pageSize = b.pageSize / 2
+
+	return true
+}
+
+func (b *BatchProcessor) Do(ctx context.Context, f func(batch []*Item) error) (int, error) {
+
+	if b.FindOptions == nil && b.FindPublishedOptions == nil {
+		b.FindOptions = new(FindOptions)
+	}
+	if b.FindOptions != nil {
+		b.pageSize = b.FindOptions.PageSize
+		b.sort = b.FindOptions.Sort
+	}
+	if b.FindPublishedOptions != nil {
+		b.pageSize = b.FindPublishedOptions.PageSize
+		b.sort = b.FindPublishedOptions.Sort
+	}
+
+	if b.pageSize == 0 {
+		b.pageSize = 128
+	}
+
+	if b.Filter != nil && (len(b.Filter.ID) > 0 || len(b.Filter.Q) > 0) && !data.Contains("_id", b.sort) {
+		b.sort = append(b.sort, "_id")
+	}
+
+	var err error
+
+	next := true
+	for next {
+
+		var batch []*Item
+
+		batch, next, err = b.next(ctx)
+		if err != nil {
+			return 0, err
+		}
+
+		if err = f(batch); err != nil {
+			return 0, err
+		}
+	}
+	return b.processed, nil
+}
diff --git a/pkg/locales/middleware/caching_middleware.go b/pkg/locales/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b4635ab4a0214f99407b67020737724ebf3c841
--- /dev/null
+++ b/pkg/locales/middleware/caching_middleware.go
@@ -0,0 +1,53 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/locales"
+)
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Locales) service.Locales {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Locales
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, locale *service.Locale) (loc *service.Locale, err error) {
+
+	loc, err = m.next.Create(ctx, locale)
+	if err == nil {
+		m.cache.Remove(loc.SpaceID)
+	}
+	return loc, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, spaceId string) (locales []*service.Locale, err error) {
+
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.([]*service.Locale), err
+	}
+	locales, err = m.next.List(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, locales)
+	}
+	return locales, err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) {
+
+	err = m.next.Delete(ctx, spaceId, localeId)
+	if err == nil {
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
diff --git a/pkg/locales/middleware/caching_middleware_test.go b/pkg/locales/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..de5e7a9f742b6336ecd5a67d529ed468236838f4
--- /dev/null
+++ b/pkg/locales/middleware/caching_middleware_test.go
@@ -0,0 +1,130 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	locmocks "git.perx.ru/perxis/perxis-go/pkg/locales/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestLocalesCache(t *testing.T) {
+
+	const (
+		loc1    = "loc1"
+		loc2    = "loc2"
+		spaceID = "spaceID"
+		size    = 5
+		ttl     = 20 * time.Millisecond
+	)
+
+	ctx := context.Background()
+
+	t.Run("List from Cache", func(t *testing.T) {
+		loc := &locmocks.Locales{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(loc)
+
+		loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once()
+
+		vl1, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+
+		vl2, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, vl1[0], vl2[0], "Ожидается что при повторном запросе объекты будут получены из кэша.")
+
+		loc.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Delete", func(t *testing.T) {
+			loc := &locmocks.Locales{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(loc)
+
+			loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается что при повторном запросе объекты будут получены из кэша.")
+
+			loc.On("Delete", mock.Anything, spaceID, loc1).Return(nil).Once()
+
+			err = svc.Delete(ctx, spaceID, loc1)
+			require.NoError(t, err)
+
+			loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 0, "Ожидается что после удаление объекты будут удалены из кеша.")
+
+			loc.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			loc := &locmocks.Locales{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(loc)
+
+			loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается что при повторном запросе объекты будут получены из кэша.")
+
+			loc.On("Create", mock.Anything, mock.Anything).Return(&locales.Locale{ID: loc2, Name: "name2", SpaceID: spaceID}, nil).Once()
+
+			_, err = svc.Create(ctx, &locales.Locale{ID: loc2, Name: "name2", SpaceID: spaceID})
+			require.NoError(t, err)
+
+			loc.On("List", mock.Anything, spaceID).
+				Return([]*locales.Locale{
+					{ID: loc1, Name: "name1", SpaceID: spaceID},
+					{ID: loc2, Name: "name2", SpaceID: spaceID},
+				}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 2, "Ожидается что после создания нового объекта данные будут удалены из кеша и получены из сервиса.")
+
+			loc.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			loc := &locmocks.Locales{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(loc)
+
+			loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается что при повторном запросе объекты будут получены из кэша.")
+
+			time.Sleep(2 * ttl)
+			loc.On("List", mock.Anything, spaceID).Return([]*locales.Locale{{ID: loc1, Name: "name1", SpaceID: spaceID}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что элементы будут получены из кэша.")
+
+			loc.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/locales/middleware/error_logging_middleware.go b/pkg/locales/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..695c91128d6f093d93f022468d464bedbd571e04
--- /dev/null
+++ b/pkg/locales/middleware/error_logging_middleware.go
@@ -0,0 +1,60 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/locales -i Locales -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements locales.Locales that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   locales.Locales
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the locales.Locales with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next locales.Locales) locales.Locales {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, locale)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, localeId)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, spaceId)
+}
diff --git a/pkg/locales/middleware/logging_middleware.go b/pkg/locales/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..fb98d25759e87d199e8c65a204f30b3acb48c1f4
--- /dev/null
+++ b/pkg/locales/middleware/logging_middleware.go
@@ -0,0 +1,142 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/locales -i Locales -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements locales.Locales that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   locales.Locales
+}
+
+// LoggingMiddleware instruments an implementation of the locales.Locales with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next locales.Locales) locales.Locales {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"locale": locale} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, locale)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":      ctx,
+		"spaceId":  spaceId,
+		"localeId": localeId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, localeId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	locales, err = m.next.List(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"locales": locales,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return locales, err
+}
diff --git a/pkg/locales/middleware/middleware.go b/pkg/locales/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..726b535247256a109f0fc5aa100e0a61cc928555
--- /dev/null
+++ b/pkg/locales/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/locales -i Locales -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.uber.org/zap"
+)
+
+type Middleware func(locales.Locales) locales.Locales
+
+func WithLog(s locales.Locales, logger *zap.Logger, log_access bool) locales.Locales {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Locales")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/locales/middleware/recovering_middleware.go b/pkg/locales/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..44b198550418034e1963fec5ccbd405e0ea12ef4
--- /dev/null
+++ b/pkg/locales/middleware/recovering_middleware.go
@@ -0,0 +1,67 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/locales -i Locales -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/locales"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements locales.Locales that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   locales.Locales
+}
+
+// RecoveringMiddleware instruments an implementation of the locales.Locales with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next locales.Locales) locales.Locales {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, locale *locales.Locale) (created *locales.Locale, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, locale)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, localeId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, localeId)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (locales []*locales.Locale, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, spaceId)
+}
diff --git a/pkg/members/middleware/caching_middleware.go b/pkg/members/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..2faa5ce433281d8d396bb5c912e4e91cfaff727f
--- /dev/null
+++ b/pkg/members/middleware/caching_middleware.go
@@ -0,0 +1,102 @@
+package service
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/members"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Members) service.Members {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Members
+}
+
+func (m cachingMiddleware) Set(ctx context.Context, orgId string, userId string, role service.Role) (err error) {
+
+	err = m.next.Set(ctx, orgId, userId, role)
+	if err == nil {
+		m.cache.Remove(makeKey(orgId, userId))
+		m.cache.Remove(makeKey(orgId))
+		m.cache.Remove(makeKey(userId))
+	}
+	return err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, orgId string, userId string) (role service.Role, err error) {
+
+	key := makeKey(orgId, userId)
+	value, e := m.cache.Get(key)
+	if e == nil {
+		return value.(service.Role), err
+	}
+	role, err = m.next.Get(ctx, orgId, userId)
+	if err == nil {
+		m.cache.Set(key, role)
+	}
+	return role, err
+}
+
+func (m cachingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) {
+
+	err = m.next.Remove(ctx, orgId, userId)
+	if err == nil {
+		m.cache.Remove(makeKey(orgId, userId))
+		m.cache.Remove(makeKey(orgId))
+		m.cache.Remove(makeKey(userId))
+	}
+	return err
+}
+
+func (m cachingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) {
+
+	err = m.next.RemoveAll(ctx, orgId)
+	if err == nil {
+		members, _ := m.ListMembers(ctx, orgId)
+		for _, member := range members {
+			m.cache.Remove(member.UserId)
+			m.cache.Remove(makeKey(orgId, member.UserId))
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*service.Member, err error) {
+
+	value, e := m.cache.Get(makeKey(orgId))
+	if e == nil {
+		return value.([]*service.Member), err
+	}
+	members, err = m.next.ListMembers(ctx, orgId)
+	if err == nil {
+		m.cache.Set(makeKey(orgId), members)
+	}
+	return members, err
+}
+
+func (m cachingMiddleware) ListOrganizations(ctx context.Context, userId string) (members []*service.Member, err error) {
+
+	value, e := m.cache.Get(makeKey(userId))
+	if e == nil {
+		return value.([]*service.Member), err
+	}
+	members, err = m.next.ListOrganizations(ctx, userId)
+	if err == nil {
+		m.cache.Set(makeKey(userId), members)
+	}
+	return members, err
+}
diff --git a/pkg/members/middleware/caching_middleware_test.go b/pkg/members/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..1844dc58ebb97b228800a95f9c9cb8b142407fb4
--- /dev/null
+++ b/pkg/members/middleware/caching_middleware_test.go
@@ -0,0 +1,147 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	mocksmembers "git.perx.ru/perxis/perxis-go/pkg/members/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestMembersCache(t *testing.T) {
+
+	const (
+		orgId  = "orgId"
+		userId = "userId"
+		size   = 5
+		ttl    = 20 * time.Millisecond
+	)
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		mbrs := &mocksmembers.Members{}
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs)
+
+		mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once()
+
+		v1, err := svc.Get(ctx, orgId, userId)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, orgId, userId)
+		require.NoError(t, err)
+		assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша, после повторного запроса get.")
+
+		mbrs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Set", func(t *testing.T) {
+			mbrs := &mocksmembers.Members{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			mbrs.On("Set", mock.Anything, orgId, userId, members.RoleMember).Return(nil).Once()
+
+			err = svc.Set(ctx, orgId, userId, members.RoleMember)
+			require.NoError(t, err)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша и получение заново из сервиса.")
+			mbrs.AssertExpectations(t)
+		})
+
+		t.Run("After Remove", func(t *testing.T) {
+			mbrs := &mocksmembers.Members{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			mbrs.On("Remove", mock.Anything, orgId, userId).Return(nil).Once()
+
+			err = svc.Remove(ctx, orgId, userId)
+			require.NoError(t, err)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после удаления из хранилища и получение заново из сервиса.")
+
+			mbrs.AssertExpectations(t)
+		})
+
+		t.Run("After RemoveAll", func(t *testing.T) {
+			mbrs := &mocksmembers.Members{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once()
+			mbrs.On("ListMembers", mock.Anything, orgId).Return([]*members.Member{{OrgId: orgId, UserId: userId, Role: members.RoleOwner}}, nil)
+
+			v1, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			mbrs.On("RemoveAll", mock.Anything, orgId).Return(nil).Once()
+
+			err = svc.RemoveAll(ctx, orgId)
+			require.NoError(t, err)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.NotMember, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после удаления из хранилища и получение заново из сервиса.")
+
+			mbrs.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			mbrs := &mocksmembers.Members{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(mbrs)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleOwner, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId, userId)
+
+			v2, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.Equal(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+
+			mbrs.On("Get", mock.Anything, orgId, userId).Return(members.RoleMember, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId, userId)
+			require.NoError(t, err)
+			assert.NotEqual(t, v2, v3, "Ожидается удаление объекта из кэша после истечения ttl и получение заново из сервиса.")
+			mbrs.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/members/middleware/error_logging_middleware.go b/pkg/members/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..08d2814bf4fc8f16f0df57462770342a93e191c8
--- /dev/null
+++ b/pkg/members/middleware/error_logging_middleware.go
@@ -0,0 +1,90 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements members.Members that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   members.Members
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the members.Members with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next members.Members) members.Members {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, orgId, userId)
+}
+
+func (m *errorLoggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.ListMembers(ctx, orgId)
+}
+
+func (m *errorLoggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.ListOrganizations(ctx, userId)
+}
+
+func (m *errorLoggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Remove(ctx, orgId, userId)
+}
+
+func (m *errorLoggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.RemoveAll(ctx, orgId)
+}
+
+func (m *errorLoggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Set(ctx, orgId, userId, role)
+}
diff --git a/pkg/members/middleware/logging_middleware.go b/pkg/members/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1b9ead6d06d694945c89638c52d4bce07e4ee938
--- /dev/null
+++ b/pkg/members/middleware/logging_middleware.go
@@ -0,0 +1,251 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements members.Members that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   members.Members
+}
+
+// LoggingMiddleware instruments an implementation of the members.Members with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next members.Members) members.Members {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"orgId":  orgId,
+		"userId": userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	role, err = m.next.Get(ctx, orgId, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"role": role,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return role, err
+}
+
+func (m *loggingMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"orgId": orgId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListMembers.Request", fields...)
+
+	members, err = m.next.ListMembers(ctx, orgId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"members": members,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListMembers.Response", fields...)
+
+	return members, err
+}
+
+func (m *loggingMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"userId": userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListOrganizations.Request", fields...)
+
+	organizations, err = m.next.ListOrganizations(ctx, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"organizations": organizations,
+		"err":           err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("ListOrganizations.Response", fields...)
+
+	return organizations, err
+}
+
+func (m *loggingMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"orgId":  orgId,
+		"userId": userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Remove.Request", fields...)
+
+	err = m.next.Remove(ctx, orgId, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Remove.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"orgId": orgId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("RemoveAll.Request", fields...)
+
+	err = m.next.RemoveAll(ctx, orgId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("RemoveAll.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"orgId":  orgId,
+		"userId": userId,
+		"role":   role} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Set.Request", fields...)
+
+	err = m.next.Set(ctx, orgId, userId, role)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Set.Response", fields...)
+
+	return err
+}
diff --git a/pkg/members/middleware/middleware.go b/pkg/members/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1aa0cfbe798f5688587ab8e4f5d4e166383d8d92
--- /dev/null
+++ b/pkg/members/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"go.uber.org/zap"
+)
+
+type Middleware func(members.Members) members.Members
+
+func WithLog(s members.Members, logger *zap.Logger, log_access bool) members.Members {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Members")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/members/middleware/recovering_middleware.go b/pkg/members/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..ec6db9f480d35215c47bff6fceef8c0e01a77447
--- /dev/null
+++ b/pkg/members/middleware/recovering_middleware.go
@@ -0,0 +1,103 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/members -i Members -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/members"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements members.Members that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   members.Members
+}
+
+// RecoveringMiddleware instruments an implementation of the members.Members with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next members.Members) members.Members {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, orgId string, userId string) (role members.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, orgId, userId)
+}
+
+func (m *recoveringMiddleware) ListMembers(ctx context.Context, orgId string) (members []*members.Member, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.ListMembers(ctx, orgId)
+}
+
+func (m *recoveringMiddleware) ListOrganizations(ctx context.Context, userId string) (organizations []*members.Member, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.ListOrganizations(ctx, userId)
+}
+
+func (m *recoveringMiddleware) Remove(ctx context.Context, orgId string, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Remove(ctx, orgId, userId)
+}
+
+func (m *recoveringMiddleware) RemoveAll(ctx context.Context, orgId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.RemoveAll(ctx, orgId)
+}
+
+func (m *recoveringMiddleware) Set(ctx context.Context, orgId string, userId string, role members.Role) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Set(ctx, orgId, userId, role)
+}
diff --git a/pkg/organizations/middleware/caching_middleware.go b/pkg/organizations/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..2017c9913b41ae8fbeb05e116abdda037fb5667e
--- /dev/null
+++ b/pkg/organizations/middleware/caching_middleware.go
@@ -0,0 +1,62 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	service "git.perx.ru/perxis/perxis-go/pkg/organizations"
+)
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Organizations) service.Organizations {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Organizations
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, org *service.Organization) (organization *service.Organization, err error) {
+	return m.next.Create(ctx, org)
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, orgId string) (organization *service.Organization, err error) {
+
+	value, e := m.cache.Get(orgId)
+	if e == nil {
+		return value.(*service.Organization), err
+	}
+	organization, err = m.next.Get(ctx, orgId)
+	if err == nil {
+		m.cache.Set(orgId, organization)
+	}
+	return organization, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, org *service.Organization) (err error) {
+
+	err = m.next.Update(ctx, org)
+	if err == nil {
+		m.cache.Remove(org.ID)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, orgId string) (err error) {
+
+	err = m.next.Delete(ctx, orgId)
+	if err == nil {
+		m.cache.Remove(orgId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, opts *options.FindOptions) (organizations []*service.Organization, total int, err error) {
+	return m.next.Find(ctx, filter, opts)
+}
diff --git a/pkg/organizations/middleware/caching_middleware_test.go b/pkg/organizations/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..59248ded4d85f904bf67e21956ad2df355706cdf
--- /dev/null
+++ b/pkg/organizations/middleware/caching_middleware_test.go
@@ -0,0 +1,119 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	mocksorgs "git.perx.ru/perxis/perxis-go/pkg/organizations/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestOrganizationsCache(t *testing.T) {
+
+	const (
+		orgId = "orgId"
+		size  = 5
+		ttl   = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		orgs := &mocksorgs.Organizations{}
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs)
+
+		orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once()
+
+		v1, err := svc.Get(ctx, orgId)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, orgId)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+		orgs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Update", func(t *testing.T) {
+			orgs := &mocksorgs.Organizations{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs)
+
+			orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			orgs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+			err = svc.Update(ctx, &organizations.Organization{ID: orgId, Name: "OrganizationUPD"})
+
+			orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "OrganizationUPD"}, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается удаление объекта из кэша после обновления и получение заново из сервиса.")
+
+			orgs.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			orgs := &mocksorgs.Organizations{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs)
+
+			orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			orgs.On("Delete", mock.Anything, mock.Anything).Return(nil).Once()
+			err = svc.Delete(ctx, orgId)
+
+			orgs.On("Get", mock.Anything, orgId).Return(nil, errNotFound).Once()
+
+			_, err = svc.Get(ctx, orgId)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается удаление объекта из кэша после удаления из хранилища и получение ошибки от сервиса.")
+
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			orgs := &mocksorgs.Organizations{}
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(orgs)
+
+			orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once()
+
+			v1, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+
+			orgs.On("Get", mock.Anything, orgId).Return(&organizations.Organization{ID: orgId, Name: "Organization"}, nil).Once()
+
+			v3, err := svc.Get(ctx, orgId)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается удаление объекта из кэша и получение заново из сервиса.")
+
+			orgs.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/organizations/middleware/error_logging_middleware.go b/pkg/organizations/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..c9631f9144db6244044379219250ca334893d0d6
--- /dev/null
+++ b/pkg/organizations/middleware/error_logging_middleware.go
@@ -0,0 +1,81 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements organizations.Organizations that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   organizations.Organizations
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the organizations.Organizations with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next organizations.Organizations) organizations.Organizations {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, org)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, orgId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, orgId)
+}
+
+func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Find(ctx, filter, opts)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, orgId string) (org *organizations.Organization, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, orgId)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, org)
+}
diff --git a/pkg/organizations/middleware/logging_middleware.go b/pkg/organizations/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f33296d5cecbee19be4bff65da4280486cd1958
--- /dev/null
+++ b/pkg/organizations/middleware/logging_middleware.go
@@ -0,0 +1,215 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements organizations.Organizations that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   organizations.Organizations
+}
+
+// LoggingMiddleware instruments an implementation of the organizations.Organizations with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next organizations.Organizations) organizations.Organizations {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx": ctx,
+		"org": org} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, org)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, orgId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"orgId": orgId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, orgId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"filter": filter,
+		"opts":   opts} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Request", fields...)
+
+	orgs, total, err = m.next.Find(ctx, filter, opts)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"orgs":  orgs,
+		"total": total,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Response", fields...)
+
+	return orgs, total, err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, orgId string) (org *organizations.Organization, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"orgId": orgId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	org, err = m.next.Get(ctx, orgId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"org": org,
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return org, err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx": ctx,
+		"org": org} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, org)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/organizations/middleware/middleware.go b/pkg/organizations/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..fe3c3d645e19cc9a0c46742be61edec6bda3882e
--- /dev/null
+++ b/pkg/organizations/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"go.uber.org/zap"
+)
+
+type Middleware func(organizations.Organizations) organizations.Organizations
+
+func WithLog(s organizations.Organizations, logger *zap.Logger, log_access bool) organizations.Organizations {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Organizations")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/organizations/middleware/recovering_middleware.go b/pkg/organizations/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..35f3a6c5f7333f7fa3664c7101d31b72be270d33
--- /dev/null
+++ b/pkg/organizations/middleware/recovering_middleware.go
@@ -0,0 +1,92 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/organizations -i Organizations -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/organizations"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements organizations.Organizations that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   organizations.Organizations
+}
+
+// RecoveringMiddleware instruments an implementation of the organizations.Organizations with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next organizations.Organizations) organizations.Organizations {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, org *organizations.Organization) (created *organizations.Organization, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, org)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, orgId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, orgId)
+}
+
+func (m *recoveringMiddleware) Find(ctx context.Context, filter *organizations.Filter, opts *options.FindOptions) (orgs []*organizations.Organization, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Find(ctx, filter, opts)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, orgId string) (org *organizations.Organization, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, orgId)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, org *organizations.Organization) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, org)
+}
diff --git a/pkg/references/middleware/client_encode_middleware.go b/pkg/references/middleware/client_encode_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1cd9cb2f22340a5d90feb95bd00accb1bae39e0f
--- /dev/null
+++ b/pkg/references/middleware/client_encode_middleware.go
@@ -0,0 +1,42 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+)
+
+// ClientEncodeMiddleware выполняет операции encode/decode для передаваемых данных
+func ClientEncodeMiddleware(colls collections.Collections) Middleware {
+	return func(refs references.References) references.References {
+		return &encodeDecodeMiddleware{
+			next:  refs,
+			colls: colls,
+		}
+	}
+}
+
+type encodeDecodeMiddleware struct {
+	next  references.References
+	colls collections.Collections
+}
+
+func (m *encodeDecodeMiddleware) Get(ctx context.Context, spaceId, envId string, refs []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) {
+	items, notfound, err = m.next.Get(ctx, spaceId, envId, refs)
+	if err == nil && len(items) > 0 {
+		for i, item := range items {
+			col, err := m.colls.Get(ctx, item.SpaceID, item.EnvID, item.CollectionID)
+			if err != nil {
+				return nil, nil, err
+			}
+
+			if item, err = item.Decode(ctx, col.Schema); err != nil {
+				return nil, nil, err
+			}
+			items[i] = item
+		}
+	}
+	return
+}
diff --git a/pkg/references/middleware/error_logging_middleware.go b/pkg/references/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a62947f240842c6afb099a9b03cc966d9bd99b5
--- /dev/null
+++ b/pkg/references/middleware/error_logging_middleware.go
@@ -0,0 +1,41 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements references.References that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   references.References
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the references.References with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next references.References) references.References {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, envId, references)
+}
diff --git a/pkg/references/middleware/logging_middleware.go b/pkg/references/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..62a0e9d5cf26c6ab0d131fff2fd443cffd8fb8f3
--- /dev/null
+++ b/pkg/references/middleware/logging_middleware.go
@@ -0,0 +1,74 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements references.References that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   references.References
+}
+
+// LoggingMiddleware instruments an implementation of the references.References with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next references.References) references.References {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":        ctx,
+		"spaceId":    spaceId,
+		"envId":      envId,
+		"references": references} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	items, notfound, err = m.next.Get(ctx, spaceId, envId, references)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"items":    items,
+		"notfound": notfound,
+		"err":      err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return items, notfound, err
+}
diff --git a/pkg/references/middleware/middleware.go b/pkg/references/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..dfed8dc0c821998df97d414876b6839a9961f6b0
--- /dev/null
+++ b/pkg/references/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+	"go.uber.org/zap"
+)
+
+type Middleware func(references.References) references.References
+
+func WithLog(s references.References, logger *zap.Logger, log_access bool) references.References {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("References")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/references/middleware/recovering_middleware.go b/pkg/references/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..b1a4eb6b629e5a48f7508edbe34f5450d5f52b10
--- /dev/null
+++ b/pkg/references/middleware/recovering_middleware.go
@@ -0,0 +1,44 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/references -i References -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	"git.perx.ru/perxis/perxis-go/pkg/references"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements references.References that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   references.References
+}
+
+// RecoveringMiddleware instruments an implementation of the references.References with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next references.References) references.References {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, envId string, references []*references.Reference) (items []*items.Item, notfound []*references.Reference, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, envId, references)
+}
diff --git a/pkg/roles/middleware/caching_middleware.go b/pkg/roles/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..19bdfe6b890806a98ac4aed81485f2f5314020b6
--- /dev/null
+++ b/pkg/roles/middleware/caching_middleware.go
@@ -0,0 +1,80 @@
+package service
+
+import (
+	"context"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/roles"
+)
+
+func makeKey(ss ...string) string {
+	return strings.Join(ss, "-")
+}
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Roles) service.Roles {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Roles
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, role *service.Role) (rl *service.Role, err error) {
+	rl, err = m.next.Create(ctx, role)
+	if err == nil {
+		m.cache.Remove(rl.SpaceID)
+	}
+	return rl, err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId string, roleId string) (rl *service.Role, err error) {
+	key := makeKey(spaceId, roleId)
+	value, e := m.cache.Get(key)
+	if e == nil {
+		return value.(*service.Role), err
+	}
+	rl, err = m.next.Get(ctx, spaceId, roleId)
+	if err == nil {
+		m.cache.Set(key, rl)
+	}
+	return rl, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, spaceId string) (roles []*service.Role, err error) {
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.([]*service.Role), err
+	}
+	roles, err = m.next.List(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, roles)
+	}
+	return roles, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, role *service.Role) (err error) {
+	err = m.next.Update(ctx, role)
+	if err == nil {
+		key := makeKey(role.SpaceID, role.ID)
+		m.cache.Remove(key)
+		m.cache.Remove(role.SpaceID)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) {
+	err = m.next.Delete(ctx, spaceId, roleId)
+	if err == nil {
+		key := makeKey(spaceId, roleId)
+		m.cache.Remove(key)
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
diff --git a/pkg/roles/middleware/caching_middleware_test.go b/pkg/roles/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..bb5496b55fae164ea3681e644c7625ab9a2b9407
--- /dev/null
+++ b/pkg/roles/middleware/caching_middleware_test.go
@@ -0,0 +1,201 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	rsmocks "git.perx.ru/perxis/perxis-go/pkg/roles/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRolesCache(t *testing.T) {
+
+	const (
+		roleID  = "roleID"
+		spaceID = "spaceID"
+		size    = 5
+		ttl     = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		rl := &rsmocks.Roles{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+		rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID, roleID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID, roleID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+		rl.AssertExpectations(t)
+	})
+
+	t.Run("List from cache", func(t *testing.T) {
+		rl := &rsmocks.Roles{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+		rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once()
+
+		vl1, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+
+		vl2, err := svc.List(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+		rl.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Update", func(t *testing.T) {
+			rl := &rsmocks.Roles{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once()
+			rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объектов из кэша.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			rl.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Update(ctx, &roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"})
+			require.NoError(t, err)
+
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}, nil).Once()
+			rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после его обновления и объект был запрошен из сервиса.")
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.")
+
+			rl.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			rl := &rsmocks.Roles{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once()
+			rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			rl.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Update(ctx, &roles.Role{ID: roleID, SpaceID: spaceID, Description: "RoleUPD"})
+			require.NoError(t, err)
+
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(nil, errNotFound).Once()
+			rl.On("List", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+
+			v3, err := svc.Get(ctx, spaceID, roleID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаления кеш объекта был удален и получена ошибка сервиса.")
+			assert.Nil(t, v3)
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаления кеш объекта был удален и получена ошибка сервиса.")
+			assert.Nil(t, vl3)
+
+			rl.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			rl := &rsmocks.Roles{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+			rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}}, nil).Once()
+
+			vl1, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объекта из кэша.")
+
+			rl.On("Create", mock.Anything, mock.Anything).Return(&roles.Role{ID: "roleID2", SpaceID: spaceID, Description: "Role2"}, nil).Once()
+
+			_, err = svc.Create(ctx, &roles.Role{ID: "roleID2", SpaceID: spaceID, Description: "Role2"})
+			require.NoError(t, err)
+
+			rl.On("List", mock.Anything, spaceID).Return([]*roles.Role{{ID: roleID, SpaceID: spaceID, Description: "Role"}, {ID: "roleID2", SpaceID: spaceID, Description: "Role2"}}, nil).Once()
+
+			vl3, err := svc.List(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 2, "Ожидает что после создания нового объекта,  кеш будет очищен и объекты запрошены заново из сервиса.")
+
+			rl.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			rl := &rsmocks.Roles{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(rl)
+
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+			rl.On("Get", mock.Anything, spaceID, roleID).Return(&roles.Role{ID: roleID, SpaceID: spaceID, Description: "Role"}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID, roleID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что объект был удален из кеша и получен заново из сервиса.")
+
+			rl.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/roles/middleware/error_logging_middleware.go b/pkg/roles/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..7afe8f1f1003031ddedb437d0422dfb4123d4526
--- /dev/null
+++ b/pkg/roles/middleware/error_logging_middleware.go
@@ -0,0 +1,80 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements roles.Roles that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   roles.Roles
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the roles.Roles with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next roles.Roles) roles.Roles {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, role)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId, roleId)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId, roleId)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, role *roles.Role) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, role)
+}
diff --git a/pkg/roles/middleware/logging_middleware.go b/pkg/roles/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..ab536b6ad9e168adc79691a3127be8a47f9e5ba7
--- /dev/null
+++ b/pkg/roles/middleware/logging_middleware.go
@@ -0,0 +1,214 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements roles.Roles that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   roles.Roles
+}
+
+// LoggingMiddleware instruments an implementation of the roles.Roles with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next roles.Roles) roles.Roles {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"role": role} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, role)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"roleId":  roleId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId, roleId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"roleId":  roleId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	role, err = m.next.Get(ctx, spaceId, roleId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"role": role,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return role, err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	roles, err = m.next.List(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"roles": roles,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return roles, err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, role *roles.Role) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":  ctx,
+		"role": role} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, role)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/roles/middleware/middleware.go b/pkg/roles/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..0a5198110dfc463b80274230a7d1cbc65283debd
--- /dev/null
+++ b/pkg/roles/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+)
+
+type Middleware func(roles.Roles) roles.Roles
+
+func WithLog(s roles.Roles, logger *zap.Logger, log_access bool) roles.Roles {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Roles")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/roles/middleware/recovering_middleware.go b/pkg/roles/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..0c0f023b6fa4ccb6f8dc4dadbf59a58bf024a6cb
--- /dev/null
+++ b/pkg/roles/middleware/recovering_middleware.go
@@ -0,0 +1,91 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/roles -i Roles -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/roles"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements roles.Roles that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   roles.Roles
+}
+
+// RecoveringMiddleware instruments an implementation of the roles.Roles with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next roles.Roles) roles.Roles {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, role *roles.Role) (created *roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, role)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string, roleId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId, roleId)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string, roleId string) (role *roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId, roleId)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, spaceId string) (roles []*roles.Role, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, role *roles.Role) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, role)
+}
diff --git a/pkg/schemaloader/context.go b/pkg/schemaloader/context.go
new file mode 100644
index 0000000000000000000000000000000000000000..7407b6b30939981f917c50d734ac770ec69b9e4e
--- /dev/null
+++ b/pkg/schemaloader/context.go
@@ -0,0 +1,30 @@
+package schemaloader
+
+import "context"
+
+type LoaderContext struct {
+	SpaceID string
+	EnvID   string
+}
+
+type loaderCtxKey struct{}
+
+func WithContext(ctx context.Context, loaderContext *LoaderContext) context.Context {
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	p, _ := ctx.Value(loaderCtxKey{}).(*LoaderContext)
+	if p != nil {
+		*p = *loaderContext
+		return ctx
+	}
+	return context.WithValue(ctx, loaderCtxKey{}, loaderContext)
+}
+
+func GetContext(ctx context.Context) *LoaderContext {
+	p, _ := ctx.Value(loaderCtxKey{}).(*LoaderContext)
+	if p == nil {
+		return new(LoaderContext)
+	}
+	return p
+}
diff --git a/pkg/schemaloader/loader.go b/pkg/schemaloader/loader.go
new file mode 100644
index 0000000000000000000000000000000000000000..e27baf4ea8b509adff88f7b9c5e99a48f15960b7
--- /dev/null
+++ b/pkg/schemaloader/loader.go
@@ -0,0 +1,92 @@
+package schemaloader
+
+import (
+	"context"
+	"net/url"
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/collections"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/schema/field"
+)
+
+// NewLoader возвращает новый загрузчик схем из коллекций
+// используется только на сервере
+// на клиенте нужно использовать методы получения полностью загруженных схем, для которых разрешение происходит на сервере
+func NewLoader(svc collections.Collections) field.Loader {
+	return &loader{svc: svc}
+}
+
+type loader struct {
+	svc collections.Collections
+}
+
+// Load - возвращает поля по референсу из коллекций (не загруженные)
+func (l *loader) Load(ctx context.Context, ref string) ([]*field.Field, error) {
+	spaceID, envID, colID, err := parseRef(ctx, ref)
+	if err != nil {
+		return nil, err
+	}
+
+	filter := &collections.Filter{ID: []string{colID}}
+
+	collections, err := l.svc.List(ctx, spaceID, envID, filter)
+	if err != nil {
+		return nil, errors.Wrapf(err, "schemaloader: failed to get collections for \"%s\"", ref)
+	}
+
+	var schemas []*field.Field
+	for _, s := range collections {
+		if s.Schema != nil {
+			schemas = append(schemas, &s.Schema.Field)
+		}
+	}
+
+	if len(schemas) == 0 {
+		return nil, errors.Errorf("schema not found \"%s\"", ref)
+	}
+
+	return schemas, nil
+}
+
+func parseRef(ctx context.Context, ref string) (spaceID, envID, colID string, err error) {
+	var u *url.URL
+	if u, err = url.Parse(ref); err != nil {
+		return
+	}
+
+	parts := strings.SplitN(u.Path, "/", 3)
+
+	switch len(parts) {
+	case 1:
+		colID = parts[0]
+	case 2:
+		spaceID = parts[0]
+		envID = "master"
+		colID = parts[1]
+	case 3:
+		spaceID = parts[0]
+		envID = parts[1]
+		colID = parts[2]
+	}
+
+	if colID == "" {
+		err = errors.Errorf("invalid schema reference \"%s\"", ref)
+	}
+
+	if loaderCtx := GetContext(ctx); loaderCtx != nil {
+		if spaceID == "" {
+			spaceID = loaderCtx.SpaceID
+		}
+
+		if envID == "" {
+			envID = loaderCtx.EnvID
+		}
+	}
+
+	if spaceID == "" {
+		err = errors.Errorf("can't identify space for reference \"%s\"", ref)
+	}
+
+	return
+}
diff --git a/pkg/schemaloader/loader_test.go b/pkg/schemaloader/loader_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f796fd8f669e31140e4171955c4d7db8723cec26
--- /dev/null
+++ b/pkg/schemaloader/loader_test.go
@@ -0,0 +1,141 @@
+package schemaloader
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+//func Test_Load(t *testing.T) {
+//
+//	const (
+//		spaceID = "SpaceID"
+//		envID   = "envID"
+//		colID   = "colID"
+//		uri     = "/colID#fieldID"
+//	)
+//
+//	t.Run("Load schema (success)", func(t *testing.T) {
+//		collSvs := &mocks.Collections{}
+//
+//		sch := schema.New(
+//			"first_name", field.String(),
+//			"last_name", field.String(),
+//		)
+//
+//		cl := &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "Collection", Schema: sch}
+//		collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) {
+//			filter := args[3].(*collections.Filter)
+//
+//			assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции")
+//		}).Return([]*collections.Collection{cl}, nil).Once()
+//
+//		loader := NewLoader(collSvs, spaceID, envID)
+//		schemas, err := loader.Load(nil, uri)
+//
+//		require.NoError(t, err, "Ожидается успешное завершение")
+//		require.Equal(t, []*field.Field{&sch.Field}, schemas, "Метод должен возвращать срез схем")
+//		collSvs.AssertExpectations(t)
+//	})
+//
+//	t.Run("Collection doesn't have schema", func(t *testing.T) {
+//		collSvs := &mocks.Collections{}
+//
+//		cl := &collections.Collection{ID: colID, SpaceID: spaceID, EnvID: envID, Name: "Collection"}
+//		collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) {
+//			filter := args[3].(*collections.Filter)
+//
+//			assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции")
+//		}).Return([]*collections.Collection{cl}, nil).Once()
+//
+//		loader := NewLoader(collSvs, spaceID, envID)
+//		schemas, err := loader.Load(nil, uri)
+//
+//		require.Error(t, err, "Ожидается ошибка")
+//		require.Contains(t, err.Error(), "schema not found")
+//		require.Nil(t, schemas, "Метод должен вернуть nil")
+//		//assert.Nil(t, schemas, "Метод должен вернуть nil")
+//		collSvs.AssertExpectations(t)
+//	})
+//
+//	t.Run("Loader not found collection", func(t *testing.T) {
+//		collSvs := &mocks.Collections{}
+//
+//		collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) {
+//			filter := args[3].(*collections.Filter)
+//
+//			assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции")
+//		}).Return([]*collections.Collection{}, nil).Once()
+//
+//		loader := NewLoader(collSvs, spaceID, envID)
+//		schemas, err := loader.Load(nil, uri)
+//
+//		require.Error(t, err, "Ожидается ошибка")
+//		require.Contains(t, err.Error(), "schema not found")
+//		require.Nil(t, schemas, "Метод должен вернуть nil")
+//		collSvs.AssertExpectations(t)
+//	})
+//
+//	t.Run("Collection service return error", func(t *testing.T) {
+//		collSvs := &mocks.Collections{}
+//
+//		collSvs.On("List", mock.Anything, spaceID, envID, mock.AnythingOfType("*collections.Filter")).Run(func(args mock.Arguments) {
+//			filter := args[3].(*collections.Filter)
+//
+//			assert.Equal(t, &collections.Filter{ID: []string{"colID"}}, filter, "Фильтр должен содержать идентификатор коллекции")
+//		}).Return(nil, errors.New("storage error")).Once()
+//
+//		loader := NewLoader(collSvs, spaceID, envID)
+//		schemas, err := loader.Load(nil, uri)
+//
+//		require.Error(t, err, "Ожидается ошибка")
+//		require.Contains(t, err.Error(), "failed to get schema")
+//		require.Nil(t, schemas, "Метод должен вернуть nil")
+//		collSvs.AssertExpectations(t)
+//	})
+//
+//	t.Run("ParseMask return error", func(t *testing.T) {
+//		collSvs := &mocks.Collections{}
+//
+//		loader := NewLoader(collSvs, spaceID, envID)
+//		schemas, err := loader.Load(nil, "")
+//
+//		require.Error(t, err, "Ожидается ошибка")
+//		require.Contains(t, err.Error(), "invalid schema reference")
+//		require.Nil(t, schemas, "Метод должен вернуть nil")
+//		collSvs.AssertExpectations(t)
+//	})
+//}
+
+func Test_parseRef(t *testing.T) {
+	ctx := WithContext(nil, &LoaderContext{SpaceID: "spc", EnvID: "env"})
+	tests := []struct {
+		ref            string
+		ctx            context.Context
+		wantSpaceID    string
+		wantEnvId      string
+		wantCollection string
+		wantErr        assert.ErrorAssertionFunc
+	}{
+		{"col", ctx, "spc", "env", "col", assert.NoError},
+		{"/col", ctx, "spc", "master", "col", assert.NoError},
+		{"spc1/env1/col", ctx, "spc1", "env1", "col", assert.NoError},
+		{"spc1/env1/col#fld", ctx, "spc1", "env1", "col", assert.NoError},
+		{"col%3f*", ctx, "spc", "env", "col?*", assert.NoError},
+		{"#fld", ctx, "spc", "env", "", assert.Error},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.ref, func(t *testing.T) {
+			gotSpaceID, gotEnvId, gotCollection, err := parseRef(tt.ctx, tt.ref)
+			if !tt.wantErr(t, err, fmt.Sprintf("parseRef(%v)", tt.ref)) {
+				return
+			}
+			assert.Equalf(t, tt.wantSpaceID, gotSpaceID, "parseRef(%v)", tt.ref)
+			assert.Equalf(t, tt.wantEnvId, gotEnvId, "parseRef(%v)", tt.ref)
+			assert.Equalf(t, tt.wantCollection, gotCollection, "parseRef(%v)", tt.ref)
+		})
+	}
+}
diff --git a/pkg/spaces/middleware/caching_middleware.go b/pkg/spaces/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..62396fc8f1e9101f92885330a50797e26dbdebe2
--- /dev/null
+++ b/pkg/spaces/middleware/caching_middleware.go
@@ -0,0 +1,106 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	service "git.perx.ru/perxis/perxis-go/pkg/spaces"
+)
+
+func orgKey(orgID string) string { return "org-" + orgID }
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Spaces) service.Spaces {
+		m := &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+
+		return m
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Spaces
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, space *service.Space) (sp *service.Space, err error) {
+
+	sp, err = m.next.Create(ctx, space)
+	if err == nil {
+		m.cache.Remove(orgKey(sp.OrgID))
+	}
+	return sp, err
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, spaceId string) (sp *service.Space, err error) {
+
+	value, e := m.cache.Get(spaceId)
+	if e == nil {
+		return value.(*service.Space), err
+	}
+	sp, err = m.next.Get(ctx, spaceId)
+	if err == nil {
+		m.cache.Set(spaceId, sp)
+	}
+	return sp, err
+}
+
+func (m cachingMiddleware) List(ctx context.Context, orgId string) (spaces []*service.Space, err error) {
+
+	value, e := m.cache.Get(orgKey(orgId))
+	if e == nil {
+		return value.([]*service.Space), err
+	}
+	spaces, err = m.next.List(ctx, orgId)
+	if err == nil {
+		m.cache.Set(orgKey(orgId), spaces)
+		for _, s := range spaces {
+			m.cache.Set(s.ID, s)
+		}
+	}
+	return spaces, err
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, space *service.Space) (err error) {
+
+	err = m.next.Update(ctx, space)
+	if err == nil {
+		value, e := m.cache.Get(space.ID)
+		if e == nil {
+			space := value.(*service.Space)
+			m.cache.Remove(orgKey(space.OrgID))
+		}
+		m.cache.Remove(space.ID)
+	}
+	return err
+}
+
+func (m cachingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *service.Config) (err error) {
+
+	err = m.next.UpdateConfig(ctx, spaceId, config)
+	if err == nil {
+		value, e := m.cache.Get(spaceId)
+		if e == nil {
+			space := value.(*service.Space)
+			m.cache.Remove(orgKey(space.OrgID))
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, spaceId string) (err error) {
+
+	err = m.next.Delete(ctx, spaceId)
+	if err == nil {
+		value, e := m.cache.Get(spaceId)
+		if e == nil {
+			space := value.(*service.Space)
+			m.cache.Remove(orgKey(space.OrgID))
+		}
+		m.cache.Remove(spaceId)
+	}
+	return err
+}
diff --git a/pkg/spaces/middleware/caching_middleware_test.go b/pkg/spaces/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2cfaf98db8713081225b3f01f39782f487292161
--- /dev/null
+++ b/pkg/spaces/middleware/caching_middleware_test.go
@@ -0,0 +1,241 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	spmocks "git.perx.ru/perxis/perxis-go/pkg/spaces/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestRolesCache(t *testing.T) {
+
+	const (
+		spaceID = "spaceID"
+		orgID   = "orgID"
+		size    = 5
+		ttl     = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	ctx := context.Background()
+
+	t.Run("Get from cache", func(t *testing.T) {
+		sp := &spmocks.Spaces{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+		sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+
+		v1, err := svc.Get(ctx, spaceID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, spaceID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+		sp.AssertExpectations(t)
+	})
+
+	t.Run("List from cache", func(t *testing.T) {
+		sp := &spmocks.Spaces{}
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+		sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once()
+
+		vl1, err := svc.List(ctx, orgID)
+		require.NoError(t, err)
+
+		vl2, err := svc.List(ctx, orgID)
+		require.NoError(t, err)
+		assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+		sp.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate cache", func(t *testing.T) {
+		t.Run("After Update", func(t *testing.T) {
+			sp := &spmocks.Spaces{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+			vl1, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			sp.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			err = svc.Update(ctx, &spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"})
+			require.NoError(t, err)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"}, nil).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "SpaceUPD"}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после обновления объекта.")
+
+			vl3, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.")
+
+			sp.AssertExpectations(t)
+		})
+
+		t.Run("After UpdateConfig", func(t *testing.T) {
+			sp := &spmocks.Spaces{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+			vl1, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			sp.On("UpdateConfig", mock.Anything, spaceID, mock.Anything).Return(nil).Once()
+
+			err = svc.UpdateConfig(ctx, spaceID, &spaces.Config{Features: []string{"feature"}})
+			require.NoError(t, err)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "SpaceUPD", Config: &spaces.Config{Features: []string{"feature"}}}, nil).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "SpaceUPD", Config: &spaces.Config{Features: []string{"feature"}}}}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается что кеш объекта был удален после обновления объекта.")
+
+			vl3, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после обновления объекта.")
+
+			sp.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			sp := &spmocks.Spaces{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+			vl1, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			sp.On("Delete", mock.Anything, spaceID).Return(nil).Once()
+
+			err = svc.Delete(ctx, spaceID)
+			require.NoError(t, err)
+
+			sp.On("Get", mock.Anything, spaceID).Return(nil, errNotFound).Once()
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{}, nil).Once()
+
+			_, err = svc.Get(ctx, spaceID)
+			require.Error(t, err)
+			assert.EqualError(t, err, "not found", "Ожидается что после удаления объекта кеш  был удален и получена ошибка от сервиса.")
+
+			vl3, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.Len(t, vl3, 0, "Ожидается что после удаления кеш объектов был удален.")
+
+			sp.AssertExpectations(t)
+		})
+
+		t.Run("After Create", func(t *testing.T) {
+			sp := &spmocks.Spaces{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}}, nil).Once()
+
+			vl1, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+
+			vl2, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.Same(t, vl1[0], vl2[0], "Ожидается при повторном запросе получение объектов из кэша.")
+
+			sp.On("Create", mock.Anything, mock.Anything).Return(&spaces.Space{ID: "spaceID2", OrgID: orgID, Name: "Space2"}, nil).Once()
+
+			_, err = svc.Create(ctx, &spaces.Space{ID: "spaceID2", OrgID: orgID, Name: "Space2"})
+			require.NoError(t, err)
+
+			sp.On("List", mock.Anything, orgID).Return([]*spaces.Space{{ID: spaceID, OrgID: orgID, Name: "Space"}, {ID: "spaceID2", OrgID: orgID, Name: "Space2"}}, nil).Once()
+
+			vl3, err := svc.List(ctx, orgID)
+			require.NoError(t, err)
+			assert.NotSame(t, vl2[0], vl3[0], "Ожидается что кеш объектов был удален после создания нового объекта.")
+
+			sp.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			sp := &spmocks.Spaces{}
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(sp)
+
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+
+			v1, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается при повторном запросе получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+			sp.On("Get", mock.Anything, spaceID).Return(&spaces.Space{ID: spaceID, OrgID: orgID, Name: "Space"}, nil).Once()
+
+			v3, err := svc.Get(ctx, spaceID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается удаление объекта из кэша по истечению ttl.")
+
+			sp.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/spaces/middleware/error_logging_middleware.go b/pkg/spaces/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..85b629ba32abe38e5030077e8fe81b04206650b0
--- /dev/null
+++ b/pkg/spaces/middleware/error_logging_middleware.go
@@ -0,0 +1,90 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements spaces.Spaces that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   spaces.Spaces
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the spaces.Spaces with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next spaces.Spaces) spaces.Spaces {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, space)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, spaceId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, spaceId)
+}
+
+func (m *errorLoggingMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.List(ctx, orgId)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, space)
+}
+
+func (m *errorLoggingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.UpdateConfig(ctx, spaceId, config)
+}
diff --git a/pkg/spaces/middleware/logging_middleware.go b/pkg/spaces/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1471ea19535139210e79968e5a4522001a0b2ae3
--- /dev/null
+++ b/pkg/spaces/middleware/logging_middleware.go
@@ -0,0 +1,248 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements spaces.Spaces that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   spaces.Spaces
+}
+
+// LoggingMiddleware instruments an implementation of the spaces.Spaces with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next spaces.Spaces) spaces.Spaces {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"space": space} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	created, err = m.next.Create(ctx, space)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"created": created,
+		"err":     err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return created, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, spaceId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	space, err = m.next.Get(ctx, spaceId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"space": space,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return space, err
+}
+
+func (m *loggingMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"orgId": orgId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Request", fields...)
+
+	spaces, err = m.next.List(ctx, orgId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"spaces": spaces,
+		"err":    err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("List.Response", fields...)
+
+	return spaces, err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":   ctx,
+		"space": space} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, space)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"spaceId": spaceId,
+		"config":  config} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("UpdateConfig.Request", fields...)
+
+	err = m.next.UpdateConfig(ctx, spaceId, config)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("UpdateConfig.Response", fields...)
+
+	return err
+}
diff --git a/pkg/spaces/middleware/middleware.go b/pkg/spaces/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..9d9d2243026f6c1152f7625e4c15bd26b5b7b5fc
--- /dev/null
+++ b/pkg/spaces/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"go.uber.org/zap"
+)
+
+type Middleware func(spaces.Spaces) spaces.Spaces
+
+func WithLog(s spaces.Spaces, logger *zap.Logger, log_access bool) spaces.Spaces {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Spaces")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/spaces/middleware/recovering_middleware.go b/pkg/spaces/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..5ca795bf5c188603794603ec11fc0c8a79479524
--- /dev/null
+++ b/pkg/spaces/middleware/recovering_middleware.go
@@ -0,0 +1,103 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/spaces -i Spaces -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/spaces"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements spaces.Spaces that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   spaces.Spaces
+}
+
+// RecoveringMiddleware instruments an implementation of the spaces.Spaces with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next spaces.Spaces) spaces.Spaces {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, space *spaces.Space) (created *spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, space)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, spaceId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, spaceId string) (space *spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, spaceId)
+}
+
+func (m *recoveringMiddleware) List(ctx context.Context, orgId string) (spaces []*spaces.Space, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.List(ctx, orgId)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, space *spaces.Space) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, space)
+}
+
+func (m *recoveringMiddleware) UpdateConfig(ctx context.Context, spaceId string, config *spaces.Config) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.UpdateConfig(ctx, spaceId, config)
+}
diff --git a/pkg/template/builder.go b/pkg/template/builder.go
new file mode 100644
index 0000000000000000000000000000000000000000..9a4ddcdb73e6b5c585d77088630096351d19b2bd
--- /dev/null
+++ b/pkg/template/builder.go
@@ -0,0 +1,171 @@
+package template
+
+import (
+	"bytes"
+	"context"
+	"text/template"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+)
+
+type Builder struct {
+	ctx     context.Context
+	cnt     *content.Content
+	SpaceID string
+	EnvID   string
+	funcs   template.FuncMap
+	data    map[string]interface{}
+}
+
+func NewBuilder(cnt *content.Content, space, env string) *Builder {
+	return &Builder{
+		ctx:     context.Background(),
+		cnt:     cnt,
+		SpaceID: space,
+		EnvID:   env,
+		funcs:   make(template.FuncMap),
+	}
+}
+
+func (b *Builder) getFuncs() template.FuncMap {
+	return template.FuncMap{
+		"lookup": getLookup(b),
+		"system": getSystem(b),
+	}
+}
+
+func (b *Builder) WithData(data map[string]interface{}) *Builder {
+	bld := *b
+	bld.data = data
+	return &bld
+}
+
+func (b *Builder) WithKV(kv ...any) *Builder {
+	bld := *b
+	if bld.data == nil {
+		bld.data = make(map[string]interface{}, 10)
+	}
+	for i := 0; i < len(kv)-1; i += 2 {
+		k, _ := kv[i].(string)
+		v := kv[i+1]
+		if k != "" && v != nil {
+			bld.data[k] = v
+		}
+	}
+	return &bld
+}
+
+func (b *Builder) GetData() map[string]interface{} {
+	return b.data
+}
+
+func (b *Builder) WithSpace(space, env string) *Builder {
+	bld := *b
+	bld.SpaceID = space
+	bld.EnvID = env
+	return &bld
+}
+
+func (b *Builder) WithContext(ctx context.Context) *Builder {
+	bld := *b
+	bld.ctx = ctx
+	return &bld
+}
+
+func (b *Builder) Context() context.Context {
+	return b.ctx
+}
+
+func (b *Builder) Template() *template.Template {
+	return template.New("main").Funcs(b.getFuncs())
+}
+
+func (b *Builder) Execute(str string, data ...any) (string, error) {
+	t := b.Template()
+	buf := new(bytes.Buffer)
+	t, err := t.Parse(str)
+	if err != nil {
+		return "", err
+	}
+	if err = t.Execute(buf, b.getData(data...)); err != nil {
+		return "", err
+	}
+	return buf.String(), nil
+}
+
+func (b *Builder) ExecuteList(str []string, data ...any) ([]string, error) {
+	t := b.Template()
+	result := make([]string, len(str))
+	buffer := new(bytes.Buffer)
+	for i, tmpl := range str {
+		if tmpl == "" {
+			continue
+		}
+		t, err := t.Parse(tmpl)
+		if err != nil {
+			return []string{}, err
+		}
+		if err = t.Execute(buffer, b.getData(data...)); err != nil {
+			return []string{}, err
+		}
+		result[i] = buffer.String()
+		buffer.Reset()
+	}
+	return result, nil
+}
+
+func (b *Builder) ExecuteMap(str map[string]interface{}, data ...any) (map[string]interface{}, error) {
+	result := make(map[string]interface{}, len(str))
+	for k, v := range str {
+		switch t := v.(type) {
+		case string:
+			value, err := b.Execute(t, data...)
+			if err != nil {
+				return nil, err
+			}
+			v = value
+		case []string:
+			values, err := b.ExecuteList(append([]string{k}, t...), data...)
+			if err != nil {
+				return nil, err
+			}
+			k = values[0]
+			vv := make([]interface{}, 0, len(t))
+			for _, val := range values[1:] {
+				vv = append(vv, val)
+			}
+			v = vv
+		}
+
+		result[k] = v
+	}
+	return result, nil
+}
+
+func (b *Builder) getData(data ...any) any {
+	if len(data) == 0 {
+		return b.data
+	}
+
+	var res map[string]interface{}
+	for _, v := range data {
+		if m, ok := v.(map[string]interface{}); ok && b.data != nil {
+			res = mergeMaps(b.data, m)
+		}
+	}
+	if res != nil {
+		return res
+	}
+
+	return data[0]
+}
+
+func mergeMaps(in ...map[string]interface{}) map[string]interface{} {
+	out := make(map[string]interface{})
+	for _, i := range in {
+		for k, v := range i {
+			out[k] = v
+		}
+	}
+	return out
+}
diff --git a/pkg/template/builder_test.go b/pkg/template/builder_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..f8e2b34440dd73259a6ccd3ce9202a556caa5298
--- /dev/null
+++ b/pkg/template/builder_test.go
@@ -0,0 +1,272 @@
+package template
+
+import (
+	"context"
+	"errors"
+	"testing"
+	"text/template"
+
+	"git.perx.ru/perxis/perxis-go/pkg/content"
+	"git.perx.ru/perxis/perxis-go/pkg/items"
+	mocksitems "git.perx.ru/perxis/perxis-go/pkg/items/mocks"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBuilder_Execute(t *testing.T) {
+	tests := []struct {
+		name    string
+		ctx     context.Context
+		cnt     *content.Content
+		SpaceID string
+		EnvID   string
+		funcs   template.FuncMap
+		str     string
+		data    any
+		want    any
+		wantErr bool
+
+		itemsCall func(itemsSvc *mocksitems.Items)
+	}{
+		{name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ .a }}", data: "world", want: "", wantErr: true},
+		{name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "", data: "", want: "", wantErr: false},
+		{name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ . }}", data: "world", want: "hello world", wantErr: false},
+		{name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "{{ . }}", data: "world", want: "world", wantErr: false},
+		{name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "", data: "world", want: "", wantErr: false},
+		{name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello", data: "world", want: "hello", wantErr: false},
+		{name: "lookup", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello, {{ lookup \"secrets.dev.key\" }}", data: "", want: "hello, Luk", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "Luk",
+				},
+			}, nil).Once()
+		}},
+		{name: "lookup with slice", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers [1 2 3]", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":    "dev",
+					"slice": []int{1, 2, 3},
+				},
+			}, nil).Once()
+		}},
+		{name: "lookup with empty Data", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "numbers {{ lookup \"secrets.dev.slice\" }}", data: "", want: "numbers <no value>", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data:         map[string]interface{}{},
+			}, nil).Once()
+		}},
+		{name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.dev.incorrect\" }}", data: "", want: "hello <no value>", wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "1234",
+				},
+			}, nil).Once()
+		}},
+		{name: "lookup not found", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.prod.pass\" }}", data: "", want: "", wantErr: true, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "prod").Return(nil, errors.New("not found")).Once()
+		}},
+		{name: "lookup without itemID", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ lookup \"secrets.pass\" }}", data: "", want: "", wantErr: true},
+		{name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: "hello {{ system.SpaceID }}", data: "", want: "hello space", wantErr: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			itemsSvc := &mocksitems.Items{}
+			if tt.itemsCall != nil {
+				tt.itemsCall(itemsSvc)
+			}
+			tt.cnt = &content.Content{
+				Items: itemsSvc,
+			}
+			b := &Builder{
+				ctx:     tt.ctx,
+				cnt:     tt.cnt,
+				SpaceID: tt.SpaceID,
+				EnvID:   tt.EnvID,
+				funcs:   tt.funcs,
+			}
+
+			got, err := b.Execute(tt.str, tt.data)
+			if tt.wantErr == true {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.want, got)
+			if tt.itemsCall != nil {
+				itemsSvc.AssertExpectations(t)
+			}
+		})
+	}
+}
+
+func TestBuilder_ExecuteList(t *testing.T) {
+	tests := []struct {
+		name    string
+		ctx     context.Context
+		cnt     *content.Content
+		SpaceID string
+		EnvID   string
+		funcs   template.FuncMap
+		str     []string
+		data    any
+		want    []string
+		wantErr bool
+
+		itemsCall func(itemsSvc *mocksitems.Items)
+	}{
+		{name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello { . }}", "go {{ . }"}, data: "world", want: []string{}, wantErr: true},
+		{name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{""}, data: "world", want: []string{""}, wantErr: false},
+		{name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ . }}", "go {{ . }}"}, data: "world", want: []string{"hello world", "go world"}, wantErr: false},
+		{name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"{{ . }}"}, data: "world", want: []string{"world"}, wantErr: false},
+		{name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{""}, data: "world", want: []string{""}, wantErr: false},
+		{name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello"}, data: "world", want: []string{"hello"}, wantErr: false},
+		{name: "lookup", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ lookup \"secrets.dev.key\" }}"}, data: "", want: []string{"hello 1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "1234",
+				},
+			}, nil).Once()
+		}},
+		{name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: []string{"hello <no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "1234",
+				},
+			}, nil).Once()
+		}},
+		{name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: []string{"hello {{ system.SpaceID }}"}, data: "", want: []string{"hello space"}, wantErr: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			itemsSvc := &mocksitems.Items{}
+			if tt.itemsCall != nil {
+				tt.itemsCall(itemsSvc)
+			}
+			tt.cnt = &content.Content{
+				Items: itemsSvc,
+			}
+			b := &Builder{
+				ctx:     tt.ctx,
+				cnt:     tt.cnt,
+				SpaceID: tt.SpaceID,
+				EnvID:   tt.EnvID,
+				funcs:   tt.funcs,
+			}
+
+			got, err := b.ExecuteList(tt.str, tt.data)
+			if tt.wantErr == true {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.want, got)
+			if tt.itemsCall != nil {
+				itemsSvc.AssertExpectations(t)
+			}
+		})
+	}
+}
+
+func TestBuilder_ExecuteMap(t *testing.T) {
+	tests := []struct {
+		name    string
+		ctx     context.Context
+		cnt     *content.Content
+		SpaceID string
+		EnvID   string
+		funcs   template.FuncMap
+		str     map[string]interface{}
+		data    any
+		want    map[string]interface{}
+		wantErr bool
+
+		itemsCall func(itemsSvc *mocksitems.Items)
+	}{
+		{name: "error", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }"}, data: "world", want: nil, wantErr: true},
+		{name: "empty", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{}, data: "", want: map[string]interface{}{}, wantErr: false},
+		{name: "#1", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world"}, wantErr: false},
+		{name: "#2", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ . }}", "go": "{{ . }}"}, data: "world", want: map[string]interface{}{"hello": "world", "go": "world"}, wantErr: false},
+		{name: "#3 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{}, data: "world", want: map[string]interface{}{}, wantErr: false},
+		{name: "#4 ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"a": "b"}, data: "world", want: map[string]interface{}{"a": "b"}, wantErr: false},
+		{name: "lookup ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.key\" }}"}, data: "", want: map[string]interface{}{"hello": "1234"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "1234",
+				},
+			}, nil).Once()
+		}},
+		{name: "lookup with incorrect field", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ lookup \"secrets.dev.incorrect\" }}"}, data: "", want: map[string]interface{}{"hello": "<no value>"}, wantErr: false, itemsCall: func(itemsSvc *mocksitems.Items) {
+			itemsSvc.On("Get", context.Background(), "space", "env", "secrets", "dev").Return(&items.Item{
+				ID:           "dev",
+				SpaceID:      "space",
+				EnvID:        "env",
+				CollectionID: "secrets",
+				Data: map[string]interface{}{
+					"id":  "dev",
+					"key": "1234",
+				},
+			}, nil).Once()
+		}},
+		{name: "system ", ctx: context.Background(), cnt: &content.Content{}, SpaceID: "space", EnvID: "env", funcs: template.FuncMap{}, str: map[string]interface{}{"hello": "{{ system.SpaceID }}"}, data: "", want: map[string]interface{}{"hello": "space"}, wantErr: false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			itemsSvc := &mocksitems.Items{}
+			if tt.itemsCall != nil {
+				tt.itemsCall(itemsSvc)
+			}
+			tt.cnt = &content.Content{
+				Items: itemsSvc,
+			}
+			b := &Builder{
+				ctx:     tt.ctx,
+				cnt:     tt.cnt,
+				SpaceID: tt.SpaceID,
+				EnvID:   tt.EnvID,
+				funcs:   tt.funcs,
+			}
+
+			got, err := b.ExecuteMap(tt.str, tt.data)
+			if tt.wantErr == true {
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+			}
+			assert.Equal(t, tt.want, got)
+			if tt.itemsCall != nil {
+				itemsSvc.AssertExpectations(t)
+			}
+		})
+	}
+}
diff --git a/pkg/template/funcs.go b/pkg/template/funcs.go
new file mode 100644
index 0000000000000000000000000000000000000000..0c320ad139e964f002b691ce097b24e70e6cfaf3
--- /dev/null
+++ b/pkg/template/funcs.go
@@ -0,0 +1,43 @@
+package template
+
+import (
+	"strings"
+
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+)
+
+// getLookup возвращает функцию для шаблонизатора для получения значений из записи коллекции
+// name указывается в виде "<collection id>.<item id>.<field>"
+// Использование в шаблонах:  {{ lookup "secrets.key.value" }}
+func getLookup(b *Builder) any {
+	return func(name string) (any, error) {
+		parsedName := strings.Split(name, ".")
+		if len(parsedName) < 3 {
+			return "", errors.Errorf("incorrect parameter \"%s\"", name)
+		}
+
+		collectionID := parsedName[0]
+		itemID := parsedName[1]
+		field := parsedName[2]
+		item, err := b.cnt.Items.Get(b.Context(), b.SpaceID, b.EnvID, collectionID, itemID)
+		if err != nil {
+			return "", errors.Wrapf(err, "failed to get \"%s\"")
+		}
+
+		if len(item.Data) > 0 {
+			if v, ok := item.Data[field]; ok {
+				return v, nil
+			}
+		}
+
+		return nil, nil
+	}
+}
+
+// getSys возвращает функцию получения System
+// Использование в шаблонах: {{ system.SpaceID }}
+func getSystem(b *Builder) any {
+	return func() *System {
+		return &System{builder: b}
+	}
+}
diff --git a/pkg/template/system.go b/pkg/template/system.go
new file mode 100644
index 0000000000000000000000000000000000000000..8f8548eb11444e72ad4f003b9225f4d4a38e4e2a
--- /dev/null
+++ b/pkg/template/system.go
@@ -0,0 +1,13 @@
+package template
+
+type System struct {
+	builder *Builder
+}
+
+func (s *System) SpaceID() string {
+	return s.builder.SpaceID
+}
+
+func (s *System) EnvID() string {
+	return s.builder.EnvID
+}
diff --git a/pkg/users/middleware/caching_middleware.go b/pkg/users/middleware/caching_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..6dc04868c75edcd77ba9bb2b3adda3f1ff3022f0
--- /dev/null
+++ b/pkg/users/middleware/caching_middleware.go
@@ -0,0 +1,91 @@
+package service
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	services "git.perx.ru/perxis/perxis-go/pkg/options"
+	service "git.perx.ru/perxis/perxis-go/pkg/users"
+)
+
+func CachingMiddleware(cache *cache.Cache) Middleware {
+	return func(next service.Users) service.Users {
+		return &cachingMiddleware{
+			cache: cache,
+			next:  next,
+		}
+	}
+}
+
+type cachingMiddleware struct {
+	cache *cache.Cache
+	next  service.Users
+}
+
+func (m cachingMiddleware) Create(ctx context.Context, create *service.User) (user *service.User, err error) {
+	return m.next.Create(ctx, create)
+}
+
+func (m cachingMiddleware) Get(ctx context.Context, userId string) (user *service.User, err error) {
+
+	value, e := m.cache.Get(userId)
+	if e == nil {
+		return value.(*service.User), err
+	}
+	user, err = m.next.Get(ctx, userId)
+	if err == nil {
+		m.cache.Set(user.ID, user)
+		for _, i := range user.Identities {
+			m.cache.Set(i, user)
+		}
+	}
+	return user, err
+}
+
+func (m cachingMiddleware) Find(ctx context.Context, filter *service.Filter, options *services.FindOptions) (users []*service.User, total int, err error) {
+	return m.next.Find(ctx, filter, options)
+}
+
+func (m cachingMiddleware) Update(ctx context.Context, update *service.User) (err error) {
+
+	err = m.next.Update(ctx, update)
+	value, e := m.cache.Get(update.ID)
+	if err == nil && e == nil {
+		usr := value.(*service.User)
+		m.cache.Remove(usr.ID)
+		for _, i := range usr.Identities {
+			m.cache.Remove(i)
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) Delete(ctx context.Context, userId string) (err error) {
+
+	err = m.next.Delete(ctx, userId)
+	value, e := m.cache.Get(userId)
+	if err == nil && e == nil {
+		usr := value.(*service.User)
+		m.cache.Remove(usr.ID)
+		for _, i := range usr.Identities {
+			m.cache.Remove(i)
+		}
+	}
+	return err
+}
+
+func (m cachingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *service.User, err error) {
+
+	value, e := m.cache.Get(identity)
+	if e == nil {
+		return value.(*service.User), err
+	}
+	user, err = m.next.GetByIdentity(ctx, identity)
+	if err == nil {
+		m.cache.Set(user.ID, user)
+		for _, i := range user.Identities {
+			m.cache.Set(i, user)
+		}
+	}
+	return user, err
+}
diff --git a/pkg/users/middleware/caching_middleware_test.go b/pkg/users/middleware/caching_middleware_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..7ad09b52fdc003eac069005a8cdb956dd58949a7
--- /dev/null
+++ b/pkg/users/middleware/caching_middleware_test.go
@@ -0,0 +1,165 @@
+package service
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/cache"
+	"git.perx.ru/perxis/perxis-go/pkg/errors"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"git.perx.ru/perxis/perxis-go/pkg/users/mocks"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+)
+
+func TestUsersCache(t *testing.T) {
+
+	const (
+		userID   = "user_id"
+		identity = "user identity"
+		size     = 5
+		ttl      = 20 * time.Millisecond
+	)
+
+	errNotFound := errors.NotFound(errors.New("not found"))
+
+	t.Run("Get from cache", func(t *testing.T) {
+		usrs := &mocks.Users{}
+		ctx := context.Background()
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs)
+
+		usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+
+		v1, err := svc.Get(ctx, userID)
+		require.NoError(t, err)
+
+		v2, err := svc.Get(ctx, userID)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+		v3, err := svc.GetByIdentity(ctx, identity)
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кэша при запросе по Identity.")
+
+		usrs.AssertExpectations(t)
+	})
+
+	t.Run("GetByIdentity from cache", func(t *testing.T) {
+		usrs := &mocks.Users{}
+		ctx := context.Background()
+
+		svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs)
+
+		usrs.On("GetByIdentity", mock.Anything, identity).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+
+		v1, err := svc.GetByIdentity(ctx, identity)
+		require.NoError(t, err)
+
+		v2, err := svc.GetByIdentity(ctx, identity)
+		require.NoError(t, err)
+		assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+		v3, err := svc.Get(ctx, userID)
+		require.NoError(t, err)
+		assert.Same(t, v2, v3, "Ожидается получение объекта из кэша при запросе по userID.")
+
+		usrs.AssertExpectations(t)
+	})
+
+	t.Run("Invalidate Cache", func(t *testing.T) {
+		t.Run("After Update", func(t *testing.T) {
+			usrs := &mocks.Users{}
+			ctx := context.Background()
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs)
+
+			usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+			usrs.On("Update", mock.Anything, mock.Anything).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+
+			v2, err := svc.GetByIdentity(ctx, identity)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			err = svc.Update(ctx, &users.User{ID: userID, Name: "New User", Identities: []string{identity}})
+			require.NoError(t, err)
+
+			usrs.On("GetByIdentity", mock.Anything, identity).Return(&users.User{ID: userID, Name: "New User", Identities: []string{identity}}, nil).Once()
+
+			v3, err := svc.GetByIdentity(ctx, identity)
+			require.NoError(t, err)
+			assert.NotSame(t, v3, v2, "Ожидается удаление объекта из кеша после обновления и получение его заново из сервиса.")
+
+			v4, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+			assert.NotSame(t, v4, v2)
+			assert.Same(t, v4, v3, "Ожидается получение нового обьекта из кеша.")
+
+			usrs.AssertExpectations(t)
+		})
+
+		t.Run("After Delete", func(t *testing.T) {
+			usrs := &mocks.Users{}
+			ctx := context.Background()
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs)
+
+			usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+			usrs.On("Delete", mock.Anything, mock.Anything).Return(nil).Once()
+
+			v1, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+
+			v2, err := svc.GetByIdentity(ctx, identity)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			err = svc.Delete(ctx, userID)
+			require.NoError(t, err)
+
+			usrs.On("GetByIdentity", mock.Anything, identity).Return(nil, errNotFound).Once()
+			usrs.On("Get", mock.Anything, userID).Return(nil, errNotFound).Once()
+
+			_, err = svc.GetByIdentity(ctx, identity)
+			require.Error(t, err)
+			assert.EqualErrorf(t, err, "not found", "Ожидается удаление объекта из кеша после удаления из хранилища и получение ошибки от сервиса.")
+
+			_, err = svc.Get(ctx, userID)
+			require.Error(t, err)
+			assert.EqualErrorf(t, err, "not found", "Ожидается удаление объекта из кеша после удаления из хранилища и получение ошибки от сервиса.")
+
+			usrs.AssertExpectations(t)
+		})
+
+		t.Run("After TTL expired", func(t *testing.T) {
+			usrs := &mocks.Users{}
+			ctx := context.Background()
+
+			svc := CachingMiddleware(cache.NewCache(size, ttl))(usrs)
+
+			usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+
+			v1, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+
+			v2, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+			assert.Same(t, v1, v2, "Ожидается получение объекта из кэша.")
+
+			time.Sleep(2 * ttl)
+
+			usrs.On("Get", mock.Anything, userID).Return(&users.User{ID: userID, Name: "User", Identities: []string{identity}}, nil).Once()
+
+			v3, err := svc.Get(ctx, userID)
+			require.NoError(t, err)
+			assert.NotSame(t, v2, v3, "Ожидается получение объекта из кэша при запросе по Identity.")
+
+			usrs.AssertExpectations(t)
+		})
+	})
+}
diff --git a/pkg/users/middleware/error_logging_middleware.go b/pkg/users/middleware/error_logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..a9084fa7a05608e45f7c5436934b5738c685a0e9
--- /dev/null
+++ b/pkg/users/middleware/error_logging_middleware.go
@@ -0,0 +1,91 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/error_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/error_log -o error_logging_middleware.go -l ""
+
+import (
+	"context"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"go.uber.org/zap"
+)
+
+// errorLoggingMiddleware implements users.Users that is instrumented with logging
+type errorLoggingMiddleware struct {
+	logger *zap.Logger
+	next   users.Users
+}
+
+// ErrorLoggingMiddleware instruments an implementation of the users.Users with simple logging
+func ErrorLoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next users.Users) users.Users {
+		return &errorLoggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *errorLoggingMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Create(ctx, create)
+}
+
+func (m *errorLoggingMiddleware) Delete(ctx context.Context, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Delete(ctx, userId)
+}
+
+func (m *errorLoggingMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Find(ctx, filter, options)
+}
+
+func (m *errorLoggingMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Get(ctx, userId)
+}
+
+func (m *errorLoggingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.GetByIdentity(ctx, identity)
+}
+
+func (m *errorLoggingMiddleware) Update(ctx context.Context, update *users.User) (err error) {
+	logger := m.logger
+	defer func() {
+		if err != nil {
+			logger.Warn("response error", zap.Error(err))
+		}
+	}()
+	return m.next.Update(ctx, update)
+}
diff --git a/pkg/users/middleware/logging_middleware.go b/pkg/users/middleware/logging_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..1fcae0626b75992ae563ff87adfd4e33edd49af6
--- /dev/null
+++ b/pkg/users/middleware/logging_middleware.go
@@ -0,0 +1,251 @@
+package service
+
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/access_log
+// gowrap: http://github.com/hexdigest/gowrap
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/access_log -o logging_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"git.perx.ru/perxis/perxis-go/pkg/auth"
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+// loggingMiddleware implements users.Users that is instrumented with logging
+type loggingMiddleware struct {
+	logger *zap.Logger
+	next   users.Users
+}
+
+// LoggingMiddleware instruments an implementation of the users.Users with simple logging
+func LoggingMiddleware(logger *zap.Logger) Middleware {
+	return func(next users.Users) users.Users {
+		return &loggingMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *loggingMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"create": create} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Request", fields...)
+
+	user, err = m.next.Create(ctx, create)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"user": user,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Create.Response", fields...)
+
+	return user, err
+}
+
+func (m *loggingMiddleware) Delete(ctx context.Context, userId string) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"userId": userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Request", fields...)
+
+	err = m.next.Delete(ctx, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Delete.Response", fields...)
+
+	return err
+}
+
+func (m *loggingMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, total int, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":     ctx,
+		"filter":  filter,
+		"options": options} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Request", fields...)
+
+	users, total, err = m.next.Find(ctx, filter, options)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"users": users,
+		"total": total,
+		"err":   err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Find.Response", fields...)
+
+	return users, total, err
+}
+
+func (m *loggingMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"userId": userId} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Request", fields...)
+
+	user, err = m.next.Get(ctx, userId)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"user": user,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Get.Response", fields...)
+
+	return user, err
+}
+
+func (m *loggingMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":      ctx,
+		"identity": identity} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetByIdentity.Request", fields...)
+
+	user, err = m.next.GetByIdentity(ctx, identity)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"user": user,
+		"err":  err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("GetByIdentity.Response", fields...)
+
+	return user, err
+}
+
+func (m *loggingMiddleware) Update(ctx context.Context, update *users.User) (err error) {
+	begin := time.Now()
+	var fields []zapcore.Field
+	for k, v := range map[string]interface{}{
+		"ctx":    ctx,
+		"update": update} {
+		if k == "ctx" {
+			fields = append(fields, zap.String("principal", fmt.Sprint(auth.GetPrincipal(ctx))))
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Request", fields...)
+
+	err = m.next.Update(ctx, update)
+
+	fields = []zapcore.Field{
+		zap.Duration("time", time.Since(begin)),
+		zap.Error(err),
+	}
+
+	for k, v := range map[string]interface{}{
+		"err": err} {
+		if k == "err" {
+			continue
+		}
+		fields = append(fields, zap.Reflect(k, v))
+	}
+
+	m.logger.Debug("Update.Response", fields...)
+
+	return err
+}
diff --git a/pkg/users/middleware/middleware.go b/pkg/users/middleware/middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..2888f263ca2083bdea91e3c3e62cde40f85e974f
--- /dev/null
+++ b/pkg/users/middleware/middleware.go
@@ -0,0 +1,28 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/middleware
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/middleware -o middleware.go -l ""
+
+import (
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"go.uber.org/zap"
+)
+
+type Middleware func(users.Users) users.Users
+
+func WithLog(s users.Users, logger *zap.Logger, log_access bool) users.Users {
+	if logger == nil {
+		logger = zap.NewNop()
+	}
+
+	logger = logger.Named("Users")
+	s = ErrorLoggingMiddleware(logger)(s)
+	if log_access {
+		s = LoggingMiddleware(logger)(s)
+	}
+	s = RecoveringMiddleware(logger)(s)
+	return s
+}
diff --git a/pkg/users/middleware/recovering_middleware.go b/pkg/users/middleware/recovering_middleware.go
new file mode 100644
index 0000000000000000000000000000000000000000..57c401b12c0b56e9f40109570c6efe59bac8d902
--- /dev/null
+++ b/pkg/users/middleware/recovering_middleware.go
@@ -0,0 +1,104 @@
+// Code generated by gowrap. DO NOT EDIT.
+// template: ../../../assets/templates/middleware/recovery
+// gowrap: http://github.com/hexdigest/gowrap
+
+package service
+
+//go:generate gowrap gen -p git.perx.ru/perxis/perxis-go/pkg/users -i Users -t ../../../assets/templates/middleware/recovery -o recovering_middleware.go -l ""
+
+import (
+	"context"
+	"fmt"
+
+	"git.perx.ru/perxis/perxis-go/pkg/options"
+	"git.perx.ru/perxis/perxis-go/pkg/users"
+	"go.uber.org/zap"
+)
+
+// recoveringMiddleware implements users.Users that is instrumented with logging
+type recoveringMiddleware struct {
+	logger *zap.Logger
+	next   users.Users
+}
+
+// RecoveringMiddleware instruments an implementation of the users.Users with simple logging
+func RecoveringMiddleware(logger *zap.Logger) Middleware {
+	return func(next users.Users) users.Users {
+		return &recoveringMiddleware{
+			next:   next,
+			logger: logger,
+		}
+	}
+}
+
+func (m *recoveringMiddleware) Create(ctx context.Context, create *users.User) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Create(ctx, create)
+}
+
+func (m *recoveringMiddleware) Delete(ctx context.Context, userId string) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Delete(ctx, userId)
+}
+
+func (m *recoveringMiddleware) Find(ctx context.Context, filter *users.Filter, options *options.FindOptions) (users []*users.User, total int, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Find(ctx, filter, options)
+}
+
+func (m *recoveringMiddleware) Get(ctx context.Context, userId string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Get(ctx, userId)
+}
+
+func (m *recoveringMiddleware) GetByIdentity(ctx context.Context, identity string) (user *users.User, err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.GetByIdentity(ctx, identity)
+}
+
+func (m *recoveringMiddleware) Update(ctx context.Context, update *users.User) (err error) {
+	logger := m.logger
+	defer func() {
+		if r := recover(); r != nil {
+			logger.Error("panic", zap.Error(fmt.Errorf("%v", r)))
+			err = fmt.Errorf("%v", r)
+		}
+	}()
+
+	return m.next.Update(ctx, update)
+}