diff --git a/docs/README.md b/docs/README.md
index 4a7523fd14..d3483710b3 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -25,6 +25,7 @@ be found in the public [user documentation].
1. [Agent Configuration](./config)
1. [RPC](./rpc)
1. [Cluster Persistence](./persistence)
+1. [Resources and Controllers](./resources)
1. [Client Agent](./client-agent)
1. [Service Discovery](./service-discovery)
1. [Service Mesh (Connect)](./service-mesh)
diff --git a/docs/persistence/README.md b/docs/persistence/README.md
index bb907d7d13..f705539fa5 100644
--- a/docs/persistence/README.md
+++ b/docs/persistence/README.md
@@ -1,5 +1,10 @@
# Cluster Persistence
+> **Note**
+> While the content of this document is still accurate, it doesn't cover the new
+> generic resource-oriented storage layer introduced in Consul 1.16. Please see
+> [Resources](../resources) for more information.
+
The cluser persistence subsystem runs entirely in Server Agents. It handles both read and
write requests from the [RPC] subsystem. See the [Consul Architecture Guide] for an
introduction to the Consul deployment architecture and the [Consensus Protocol] used by
diff --git a/docs/resources/README.md b/docs/resources/README.md
new file mode 100644
index 0000000000..1356da2079
--- /dev/null
+++ b/docs/resources/README.md
@@ -0,0 +1,111 @@
+# Resources
+
+Consul 1.16 introduced a set of [generic APIs] for managing resources, and a
+[controller runtime] for building functionality on top of them.
+
+[generic APIs]: ../../proto-public/pbresource/resource.proto
+[controller runtime]: ../../internal/controller
+
+Previously, adding features to Consul involved making changes at every layer of
+the stack, including: HTTP handlers, RPC handlers, MemDB tables, Raft
+operations, and CLI commands.
+
+This architecture made sense when the product was maintained by a small core
+group who could keep the entire system in their heads, but presented significant
+collaboration, ownership, and onboarding challenges when our contributor base
+expanded to many engineers, across several teams, and the product grew in
+complexity.
+
+In the new model, teams can work with much greater autonomy by building on top
+of a shared platform and own their resource types and controllers.
+
+## Architecture Overview
+
+![architecture diagram](./architecture-overview.png)
+
+[source](https://whimsical.com/state-store-v2-UKE6SaEPXNc4UrZBrZj4Kg)
+
+Our resource-oriented architecture comprises the following components:
+
+#### Resource Service
+
+[Resource Service](../../proto-public/pbresource/resource.proto) is a gRPC
+service that contains the shared logic for creating, reading, updating,
+deleting, and watching resources. It will be consumed by controllers, our
+Kubernetes integration, the CLI, and mapped to an HTTP+JSON API.
+
+#### Type Registry
+
+[Type Registry](../../internal/resource/registry.go) is where teams register
+their resource types, along with hooks for performing structural validation,
+authorization, etc.
+
+#### Storage Backend
+
+[Storage Backend](../../internal/storage/storage.go) is an abstraction over
+low-level storage primitives. Today, there are two implementations (Raft and
+an in-memory backend for tests) but in the future, we envisage external storage
+systems such as the Kubernetes API or an RDBMS could be supported which would
+reduce operational complexity for our customers.
+
+#### Controllers
+
+[Controllers](../../internal/controller/api.go) implement Consul's business
+logic using asynchronous control loops that respond to changes in resources.
+
+## Raft Storage Backend
+
+Our [Raft Storage Backend](../../internal/storage/raft/backend.go) integrates
+with the existing Raft machinery (e.g. FSM) used by the [old state store]. It
+also transparently forwards writes and strongly consistent reads to the leader
+over gRPC.
+
+There's quite a lot going on here, so to dig into the details, let's take a look
+at how a write operation is handled.
+
+[old state store]: ../persistence/
+
+![raft storage backend diagram](./raft-backend.png)
+
+[source](https://whimsical.com/state-store-v2-UKE6SaEPXNc4UrZBrZj4Kg)
+
+#### Steps 1 & 2
+
+User calls the resource service's `Write` endpoint, on a Raft follower, which
+in-turn calls the storage backend's `WriteCAS` method.
+
+#### Steps 3 & 4
+
+The storage backend determines that the current server is a Raft follower, and
+forwards the operation to the leader via a gRPC [forwarding service] listening
+on the multiplexed RPC port ([`ports.server`]).
+
+[forwarding service]: ../../proto/private/pbstorage/raft.proto
+[`ports.server`]: https://developer.hashicorp.com/consul/docs/agent/config/config-files#server_rpc_port
+
+#### Step 5
+
+The leader's storage backend serializes the operation to protobuf and applies it
+to the Raft log. As we need to share the Raft log with the old state store, we go
+through the [`consul.raftHandle`](../../agent/consul/raft_handle.go) and
+[`consul.Server`](../../agent/consul/server/server.go) which applies a msgpack
+envelope and type byte prefix.
+
+#### Step 6
+
+Raft consensus happens! Once the log has been committed, it is applied to the
+[FSM](../../agent/consul/fsm/fsm.go) which calls the storage backend's `Apply`
+method to apply the protobuf-encoded operation to the [`inmem.Store`].
+
+[`inmem.Store`]: ../../internal/storage/inmem/store.go
+
+#### Steps 7, 8, 9
+
+At this point, the operation is complete. The forwarding service returns a
+successful response, as does the follower's storage backend, and the user
+gets a successful response too.
+
+#### Steps 10 & 11
+
+Asynchronously, the log is replicated to followers and applied to their storage
+backends.
diff --git a/docs/resources/architecture-overview.png b/docs/resources/architecture-overview.png
new file mode 100644
index 0000000000..656844393e
Binary files /dev/null and b/docs/resources/architecture-overview.png differ
diff --git a/docs/resources/raft-backend.png b/docs/resources/raft-backend.png
new file mode 100644
index 0000000000..42499fa86a
Binary files /dev/null and b/docs/resources/raft-backend.png differ
diff --git a/internal/storage/raft/backend.go b/internal/storage/raft/backend.go
index 387e35973c..4e1cd05bbb 100644
--- a/internal/storage/raft/backend.go
+++ b/internal/storage/raft/backend.go
@@ -44,6 +44,9 @@ import (
// intended to communicate over Consul's multiplexed server port (which handles
// TLS).
//
+// For more information, see here:
+// https://github.com/hashicorp/consul/tree/main/docs/resources#raft-storage-backend
+//
// You must call Run before using the backend.
func NewBackend(h Handle, l hclog.Logger) (*Backend, error) {
s, err := inmem.NewStore()
diff --git a/proto-public/pbresource/resource.pb.go b/proto-public/pbresource/resource.pb.go
index 76bf1be45c..4a46d1ea8b 100644
--- a/proto-public/pbresource/resource.pb.go
+++ b/proto-public/pbresource/resource.pb.go
@@ -7,6 +7,8 @@
// protoc (unknown)
// source: pbresource/resource.proto
+// For more information, see: https://github.com/hashicorp/consul/tree/main/docs/resources
+
package pbresource
import (
diff --git a/proto-public/pbresource/resource.proto b/proto-public/pbresource/resource.proto
index 038f96c7a5..9e65647587 100644
--- a/proto-public/pbresource/resource.proto
+++ b/proto-public/pbresource/resource.proto
@@ -3,6 +3,7 @@
syntax = "proto3";
+// For more information, see: https://github.com/hashicorp/consul/tree/main/docs/resources
package hashicorp.consul.resource;
import "annotations/ratelimit/ratelimit.proto";