diff --git a/app/services/service_response.rb b/app/services/service_response.rb
index da4773ab9c7827f54bfad8d18c85546a25a796b2..86efc01bd308e8b3da2b3c7983b0349e7ec8f10f 100644
--- a/app/services/service_response.rb
+++ b/app/services/service_response.rb
@@ -56,6 +56,10 @@ def to_h
       reason: reason)
   end
 
+  def deconstruct_keys(keys)
+    to_h.slice(*keys)
+  end
+
   def success?
     status == :success
   end
diff --git a/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb b/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb
index 2b45c0e0fc79e3b0926ec0fb9dc38d2dcf1863ab..c6b7d6f8ccdd0bee2d86fb81ad3bb626a05d7744 100644
--- a/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb
+++ b/ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb
@@ -23,7 +23,6 @@ class WorkspacesResolver < ::Resolvers::BaseResolver
 
       def resolve(**args)
         unless ::Feature.enabled?(:remote_development_feature_flag)
-          # noinspection RubyMismatchedArgumentType
           raise_resource_not_available_error! "'remote_development_feature_flag' feature flag is disabled"
         end
 
diff --git a/ee/app/services/remote_development/agent_config/update_service.rb b/ee/app/services/remote_development/agent_config/update_service.rb
index 006ec50fe0955b92b5baf5b1088b6f89e21663b1..f3e80a5b398af6839acb13c1b8980bbb94bc3237 100644
--- a/ee/app/services/remote_development/agent_config/update_service.rb
+++ b/ee/app/services/remote_development/agent_config/update_service.rb
@@ -3,6 +3,12 @@
 module RemoteDevelopment
   module AgentConfig
     class UpdateService
+      # NOTE: This constructor intentionally does not follow all of the conventions from
+      #       https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
+      #       suggesting that the dependencies be passed via the constructor.
+      #
+      #       See "Stateless Service layer classes" in ee/lib/remote_development/README.md for more details.
+
       # @param [Clusters::Agent] agent
       # @param [Hash] config
       # @return [Hash, FalseClass]
diff --git a/ee/app/services/remote_development/service_response_factory.rb b/ee/app/services/remote_development/service_response_factory.rb
new file mode 100644
index 0000000000000000000000000000000000000000..413d4bbf91ab1897422e31ebc30750e3ec1ae364
--- /dev/null
+++ b/ee/app/services/remote_development/service_response_factory.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  module ServiceResponseFactory
+    # @param [Hash] response_hash
+    # @return [ServiceResponse]
+    def create_service_response(response_hash)
+      # NOTE: We are not using the ServiceResponse class directly in the Domain Logic layer, but instead we
+      # have the Domain Logic layer return a hash with the necessary entries to create a ServiceResponse object.
+      # This is because:
+      #
+      # 1. It makes the specs for the classes on the outer edge of the Domain Logic layer more concise
+      #    and straightforward if they can assert on plain hash return values rather than unpacking ServiceResponse
+      #    objects.
+      # 2. We can use this as a centralized place to do some type-checking of the values to be contained in
+      #    the ServiceResponse (this could be added to ServiceResponse in the future if we choose, but it is
+      #    currently dependent upon the experimental rightward assignment feature).
+      # 3. This would technically be a circular dependency, since the ServiceResponse class is part of the
+      #    Service layer, but the Service layer calls the Domain Logic layer.
+      #
+      # We may change this in the future as we evolve the abstractions around the Service layer,
+      # but for now we are keeping them strictly decoupled.
+      #
+      # See ee/lib/remote_development/README.md for more context.
+
+      validate_response_hash(response_hash)
+      ServiceResponse.new(**response_hash)
+    end
+
+    private
+
+    # @param [Hash] response_hash
+    # @return [void]
+    # @raise [RuntimeError]
+    def validate_response_hash(response_hash)
+      # Explicitly assign nil to all valid values, so we can type-check the values using rightward assignment,
+      #    which requires that nil values must be explicitly set.
+      hash = { status: nil, payload: nil, message: nil, reason: nil }.merge(response_hash)
+
+      # Type-check response using rightward assignment
+      hash => {
+        status: Symbol => status,
+        payload: (Hash | NilClass) => payload,
+        message: (String | NilClass) => message,
+        reason: (Symbol | NilClass)=> reason,
+      }
+
+      raise "Invalid 'status:' value for response: #{status}" unless [:success, :error].include?(status)
+
+      # NOTE: These rules are more strict than the ones in ServiceResponse, but we want to enforce this pattern of
+      #       usage within the Remote Development domain.
+      if status == :success
+        raise "'reason:' cannot specified if 'status:' is :success" if reason
+
+        raise "'message:' cannot specified if 'status:' is :success" if message
+
+        raise "'payload:' must specified if 'status:' is :success" if payload.nil?
+      else
+        raise "'reason:' must be specified if 'status:' is :error" if reason.nil?
+
+        raise "'message:' must be specified if 'status:' is :error" if message.nil?
+
+        raise "'payload:' cannot be specified if 'status:' is :error" if payload
+      end
+
+      nil
+    end
+  end
+end
diff --git a/ee/app/services/remote_development/workspaces/create_service.rb b/ee/app/services/remote_development/workspaces/create_service.rb
index 993fbf6a761c92df31b8379cbd75fc1e1e27a304..0288962886914ef0c461eb4cfc5e27f5b738974d 100644
--- a/ee/app/services/remote_development/workspaces/create_service.rb
+++ b/ee/app/services/remote_development/workspaces/create_service.rb
@@ -7,19 +7,10 @@ class CreateService
 
       # NOTE: This constructor intentionally does not follow all of the conventions from
       #       https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
-      #       suggesting that the dependencies be passed via the constructor. This is because
-      #       the RemoteDevelopment feature architecture follows a more pure-functional style,
-      #       by avoiding instance variables and instance state and preferring to pass data
-      #       directly in method calls rather than via constructor. We also don't use any of the
-      #       provided superclasses like BaseContainerService or its descendants, because all of the
-      #       domain logic is isolated and decoupled to the architectural tier below this,
-      #       i.e. in the `*Processor` classes, and therefore these superclasses provide nothing
-      #       of use. However, we do still conform to the convention of passing the current_user
-      #       in the constructor, since this convention is related to security, and worth following
-      #       the existing patterns and principle of least surprise.
+      #       suggesting that the dependencies be passed via the constructor.
       #
-      #       See https://gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/-/blob/main/doc/remote-development-feature-architectural-standards.md
-      #       for more discussion on this topic.
+      #       See "Stateless Service layer classes" in ee/lib/remote_development/README.md for more details.
+
       # @param [User] current_user
       # @return [void]
       def initialize(current_user:)
diff --git a/ee/app/services/remote_development/workspaces/reconcile_service.rb b/ee/app/services/remote_development/workspaces/reconcile_service.rb
index 0ec3561c209c672023c5491fef6d8075dc40e027..95855fb101bbbae3920bd8d4ab3cfa35976aa362 100644
--- a/ee/app/services/remote_development/workspaces/reconcile_service.rb
+++ b/ee/app/services/remote_development/workspaces/reconcile_service.rb
@@ -3,20 +3,11 @@
 module RemoteDevelopment
   module Workspaces
     class ReconcileService
-      # NOTE: This class intentionally does not follow the constructor conventions from
+      # NOTE: This constructor intentionally does not follow all of the conventions from
       #       https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
-      #       suggesting that the dependencies be passed via the constructor. This is because
-      #       the RemoteDevelopment feature architecture follows a more pure-functional style,
-      #       directly in method calls rather than via constructor. We also don't use any of the
-      #       provided superclasses like BaseContainerService or its descendants, because all of the
-      #       domain logic is isolated and decoupled to the architectural tier below this,
-      #       i.e. in the `*Processor` classes, and therefore these superclasses provide nothing
-      #       of use. In this case we also do not even pass the `current_user:` parameter, because this
-      #       service is called from GA4K kas from an internal kubernetes endpoint, and thus there
-      #       is no current_user in context. Therefore we have no need for a constructor at all.
+      #       suggesting that the dependencies be passed via the constructor.
       #
-      #       See https://gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/-/blob/main/doc/remote-development-feature-architectural-standards.md
-      #       for more discussion on this topic.
+      #       See "Stateless Service layer classes" in ee/lib/remote_development/README.md for more details.
 
       # @param [Clusters::Agent] agent
       # @param [Hash] params
diff --git a/ee/app/services/remote_development/workspaces/update_service.rb b/ee/app/services/remote_development/workspaces/update_service.rb
index d701a8505ce3aff83809c7d43f3abcddcfce1efa..d788375446f3ae24ae76d6df0b1a986ae1cc2fac 100644
--- a/ee/app/services/remote_development/workspaces/update_service.rb
+++ b/ee/app/services/remote_development/workspaces/update_service.rb
@@ -3,23 +3,15 @@
 module RemoteDevelopment
   module Workspaces
     class UpdateService
+      include ServiceResponseFactory
       attr_reader :current_user
 
       # NOTE: This constructor intentionally does not follow all of the conventions from
       #       https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes
-      #       suggesting that the dependencies be passed via the constructor. This is because
-      #       the RemoteDevelopment feature architecture follows a more pure-functional style,
-      #       by avoiding instance variables and instance state and preferring to pass data
-      #       directly in method calls rather than via constructor. We also don't use any of the
-      #       provided superclasses like BaseContainerService or its descendants, because all of the
-      #       domain logic is isolated and decoupled to the architectural tier below this,
-      #       i.e. in the `*Processor` classes, and therefore these superclasses provide nothing
-      #       of use. However, we do still conform to the convention of passing the current_user
-      #       in the constructor, since this convention is related to security, and worth following
-      #       the existing patterns and principle of least surprise.
+      #       suggesting that the dependencies be passed via the constructor.
       #
-      #       See https://gitlab.com/gitlab-org/remote-development/gitlab-remote-development-docs/-/blob/main/doc/remote-development-feature-architectural-standards.md
-      #       for more discussion on this topic.
+      #       See "Stateless Service layer classes" in ee/lib/remote_development/README.md for more details.
+
       # @param [User] current_user
       # @return [void]
       def initialize(current_user:)
@@ -30,22 +22,12 @@ def initialize(current_user:)
       # @param [Hash] params
       # @return [ServiceResponse]
       def execute(workspace:, params:)
-        return ServiceResponse.error(message: 'Unauthorized', reason: :unauthorized) unless authorized?(workspace)
-
-        payload, error = RemoteDevelopment::Workspaces::Update::UpdateProcessor.new.process(
-          workspace: workspace,
-          params: params
-        )
+        response_hash = Update::Main.main(workspace: workspace, current_user: current_user, params: params)
 
-        return ServiceResponse.error(message: error.message, reason: error.reason) if error
+        # Type-check payload using rightward assignment
+        response_hash[:payload] => { workspace: Workspace } if response_hash[:payload]
 
-        ServiceResponse.success(payload: payload)
-      end
-
-      # @param [RemoteDevelopment::Workspace] workspace
-      # @return [TrueClass, FalseClass]
-      def authorized?(workspace)
-        current_user&.can?(:update_workspace, workspace)
+        create_service_response(response_hash)
       end
     end
   end
diff --git a/ee/lib/remote_development/README.md b/ee/lib/remote_development/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..f6514a1ae6ff01109434de856b16bb5b54257932
--- /dev/null
+++ b/ee/lib/remote_development/README.md
@@ -0,0 +1,322 @@
+# `ee/lib/remote_development` overview
+
+## Table of Contents
+
+- [TL;DR and Quickstart](#tldr-and-quickstart)
+- [Overview](#overview)
+  - [Layered architecture](#layered-architecture)
+- [Type safety](#type-safety)
+- [Functional patterns](#functional-patterns)
+- [Object-Oriented patterns](#object-oriented-patterns)
+- [Railway Oriented Programming and the Result Class](#railway-oriented-programming-and-the-result-class)
+- [Benefits](#benefits)
+- [Differences from standard GitLab patterns](#differences-from-standard-gitlab-patterns)
+- [FAQ](#faq)
+
+## TL;DR and Quickstart
+
+- All the domain logic lives under `ee/lib/remote_development`. Unless you are changing the DB schema or API structure, your changes will probably be made here.
+- The `Main` class is the entry point for each sub-module, and is found at `ee/lib/remote_development/**/main.rb`
+- Have a look through the ["Railway Oriented Programming"](https://fsharpforfunandprofit.com/rop/) presentation slides (middle of that page) to understand the patterns used in the Domain Logic layer.
+- Use `scripts/remote_development/run-smoke-test-suite.sh` locally, to get a faster feedback than pushing to CI and waiting for a build.
+- Use `scripts/remote_development/run-e2e-tests.sh` to easily run the QA E2E tests.
+- If you use [RubyMine](https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/rubymine/), you will get a lot of extra help, because we try to keep the `Inspect Code` clean and green for all Remote Development files, and also maintain YARD annotations, which means you will get fast in-IDE feedback about many errors such as type violations, etc, which are not caught by the standard Gitlab static linters such as RuboCop, ESLint, etc.
+
+## Overview
+
+### Layered architecture
+
+In the Remote Development feature, we strive to maintain a clean, layered architecture with the business logic at the center.
+
+```mermaid
+flowchart TB
+    direction TB
+    Client --> r
+    subgraph r[Rails Routing/Controllers]
+        direction TB
+        subgraph g[Grape/GraphQL API]
+            direction TB
+            subgraph s[Remote Development Services]
+                direction TB
+                dl[Domain Logic]
+            end
+        end
+        dl --> ActiveRecord
+        dl --> o[Other domain services]
+    end
+```
+
+The layers are designed to be _loosely coupled_, with clear interfaces and no circular dependencies, to the extent this is possible within the current GitLab Rails monolith architecture.
+
+An example of this is how we avoid coupling the Domain Logic layer to the Service layer's `ServiceResponse` concern, which would technically be a circular dependency, since the `ServiceResponse` is owned by the Service layer. Instead of using the ServiceResponse class directly in the Domain Logic layer, we have the Domain Logic layer return a hash with the necessary entries to create a ServiceResponse object. This also provides other benefits. See the comments in [`ee/app/services/remote_development/service_response_factory.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/app/services/remote_development/service_response_factory.rb#L12-12) for more details.
+
+This overall approach is aligned with [our direction towards a more modular monolith](https://docs.gitlab.com/ee/architecture/blueprints/modular_monolith/). See that document for more information on
+the motivations and patterns. Specifically, see the `References` sub-page and reference to the the [`hexagonal architecture ports and adapters`](https://www.google.com/search?q=hexagonal+architecture+ports+and+adapters&tbm=isch) pattern, which includes [this article with an example of this architecture](https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/)
+
+## Type safety
+
+The Remote Development domain leverages type safety where possible and pragmatic. This allows us to have some run-time safety nets in addition to test coverage, and also helps RubyMine provide useful warnings when the wrong types are used.
+
+Although Ruby is traditionally weakly-typed, without null safety and little support for type inference, there are several options to achieve a type safety net, especially since the 3.0 release.
+
+### Type checking
+
+- We use [**YARD annotations**](https://yardoc.org/) for method signatures. This is used to provide warnings in IDEs such as RubyMine when incorrect types are being used. We are currently trying this out, we may not continue, or we may replace or augment it with other type safety approaches in the future.
+- We do not attempt to use RBS or Sorbet (yet), as these require a more extensive investment and wider impacting changes, so any work in this area should be coordinated with the rest of the codebase.
+
+### Union types
+
+We also simulate ["Union Types"](https://en.wikipedia.org/wiki/Union_type) in Ruby. We do this through the use of a module which defines multiple class constants of the same type. The `RemoteDevelopment::Messages` module is an example of this. 
+
+### Pattern matching with types
+
+#### Pattern matching
+
+- The `case ... in` structure can be used to pattern-match on types. When used with the approach of throwing an exception in the `else` clause, this can provide exhaustive type checking at runtime.
+
+#### Rightward assignment pattern matching and destructuring with types
+    
+Example: Given a `Hash` `x` with an entry `y` which is an `Integer`, the following code would destructure the integer into `i`:
+
+```ruby
+x = {y: 1}
+x => {y: Integer => i}
+puts i # 1
+``` 
+
+If `y` was not an integer type, a `NoMatchingPatternError` runtime exception with a descriptive message would be thrown:
+
+```ruby
+x = {y: "Not an Integer"}
+x => {y: Integer => i} #  {:y=>"Not an Integer"}: Integer === "Not an integer" does not return true (NoMatchingPatternError)
+``` 
+
+- This is a powerful new feature of Ruby 3 which allows for type safety without requiring the use of type safety tools such as RBS or Sorbet.
+- Although rightward pattern matching with types is still an [experimental feature](https://rubychangelog.com/versions-latest/), it has been stable with [little negative feedback](https://bugs.ruby-lang.org/issues/17260)).
+- Also, Matz has [stated his committment to the support of rightward assignement for pattern matching](https://bugs.ruby-lang.org/issues/17260#note-1). 
+- But even if the experimental support for types in rightward assignment was removed, it would be straightforward to change all occurrences to remove the types and go back to regular rightward assignment. We would just lose the type safety.
+
+Also note that `#deconstruct_keys` must be implemented in order to use these pattern matching features.
+
+#### Pattern matching and destructuring without types
+
+Also note that the destructuring a hash or array, even without the type checks (e.g. `x => {y: i}`), is still a form of type safety, because it will raise a `NoMatchingPatternKeyError` exception if the hash or array does not have the expected structure.
+
+### Null safety
+
+When accessing a `Hash` entry by key, where we expect that the value must present (or otherwise an upstream bug exists), we prefer to use `Hash#fetch`
+instead of `Hash#[]`. 
+
+However, this is only necessary in cases cases where it is not possible or desireable to otherwise use type safety or 
+
+## Functional patterns
+
+The domain layer of the Remote Development feature uses some Functional Programming patterns.
+
+Although these patterns may not be common in Rails apps or the GitLab Rails monolith, they fully supported in Ruby, and are commonly used in many other languages, including other lanaguages used within GitLab, such as Javascript, Golang, and Rust. The functional patterns have benefits which we want to leverage, such as the following.
+
+However, we try to avoid functional patterns which would add little value, and/or could be confusing and difficult to understand even if technically supported in Ruby. [`currying`](https://www.rubydoc.info/stdlib/core/Method:curry) would be an example of this.
+
+### Immutable state
+
+Wherever possible, we use immutable state. This leads to fewer state-related bugs, and code which is easier to understand, test, and debug. This is a common pattern, and is the basis of many widely used frameworks, such as Redux and Vuex. It is also the basis of architectures such as Event Sourcing, which we [may consider for some GitLab features/domains in the future as we move towards a modular monolith](https://docs.gitlab.com/ee/architecture/blueprints/modular_monolith/references.html#reference-implementations--guides).
+
+### Higher order functions
+
+["Higher order functions"](https://en.wikipedia.org/wiki/Higher-order_function) are the basis of many (or most) functional patterns. Ruby supports this by allowing lambdas, procs, or method object references to be passed as arguments to other methods.
+
+In the Remote Development feature, we accomplish this by passing lambdas or "singleton" (class) `Method` objects as arguments. 
+
+Note that we do not use procs (and enforce their non-usage), because of their behavior with regard to arguments and the `return` keyword.
+
+### Pure functions
+
+We rely on ["pure functions"](https://en.wikipedia.org/wiki/Pure_function), which are necessary to support and enforce functional patterns such as immutable state and higher order functions.
+
+Instance variables are are a form of state, and are incompatible with the usage of pure functions, so we avoid their usage except in ["value object"](#value-objects) classes, which are intended only to encapsulate state in an object, but have no business logic in the class.
+
+In Ruby, higher order functions are implemented and enforced through the usage of "singleton" or class methods, which by definition do not allow the usage of constructors and instance variables, and therefore cannot contain or reference state (unless you try to set state in a class variable, which you should never do in the context of a Rails request anyway 😉).
+
+### Concurrency and Parallelism
+
+By using patterns such as immutable state and pure functions, we are able to support concurrency and parallelism in the domain logic, which Ruby supports though various standard library features.
+
+This may be useful in the future for the Remote Development feature, as operations such as reconciliation of workspace state involve processing data for many independent workspaces in a single request.
+
+### Error Handling
+
+The domain logic of the Remote Development feature is based on the
+["Railway Oriented Programming"](https://fsharpforfunandprofit.com/rop/) pattern, through the usage of a standard [`Result` class](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/result.rb) as found in many programming languages (ours is based on the [Rust implementation](https://doc.rust-lang.org/std/result/index.html)).
+
+This Railway Oriented Programming pattern allows us to keep the business logic decoupled from logging, error handling, and tracking/observability concerns, and chain these cohesive business logic operations together in a decoupled way.
+
+## Object-Oriented patterns
+
+Although the functional patterns above are used when they provide benefits, we otherwise still try to adhere to standard OO/Ruby/Rails idioms and patterns, for example:
+
+### Value Objects
+
+When we need to pass data around, we encapsulate it in objects. This may be a standard libary class such as `Hash` or `String`, or it may be a custom class which we create.
+
+The custom classes are a form of the ["Value Object](https://thoughtbot.com/blog/value-object-semantics-in-ruby) pattern. Currently, `RemoteDevelopment::Message` is the only example of this (NOTE: `error.rb` is also one, but it is being deprecated in favor of `Message`).
+
+For these custom value object classes, the `#==` method should be implemented.
+
+### Mixins
+
+Mixins (implemented as modules in standard Ruby or "concerns" in Rails) are a common pattern for sharing logic in Ruby and Rails.
+
+We prefer mixins/modules instead of superclasses/inheritance for sharing code. This is because modules (which are actually a form of [multiple inheritance](https://en.wikipedia.org/wiki/Multiple_inheritance)) provide more flexibility than single inheritance.
+
+### Other OO patterns
+
+We _currently_ do not make heavy or explicit use of [dependency injection](https://en.wikipedia.org/wiki/Dependency_injection) or [composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance). But, we may adopt these patterns in the future if they provide benefits. 
+
+## Railway Oriented Programming and the Result class
+
+The Domain Logic layer uses the ["Railway Oriented Programming"](https://fsharpforfunandprofit.com/rop/) pattern (AKA "ROP"). See the presentation slides on that page for an overview which explains the motivation and implementation of this pattern.
+
+### Result class
+
+To support this pattern, we have created a standard, reusable [`Result` class](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/result.rb).
+
+This is a very common pattern in many languages, and our `Result` class naming and usage is based on a subset of the [Rust implementation](https://doc.rust-lang.org/std/result/index.html). It's actually a monad, but you don't have to know anything about that word in order to use it. It's [definitely _not_ a burrito](https://www.google.com/search?q=monads+are+not+burritos).
+
+The main idea of `Result` is that it is an abstraction which encapsulates whether an operation was successful ("`ok`") or failed ("`err`"). In either case, it will contain a `value`, representing either the output of the successful operation, or some information about the failure.
+
+The `Result#and_then` and `Result#map` methods are [higher order functions](#higher-order-functions) which support the Railway Oriented Programming pattern. They allow "function references" (which are Ruby lambdas or singleton/class `Method` object instances) to be passed, which allows them to be "chained" together, with a `Result` and its contained value being passed along the chain. If any step in the chain fails, the chain exits early.
+
+The only difference between `Result#and_then` and `Result#map` is that reference passed to `#and_then` needs to support the possibility of an `err` failure, but the reference passed to `#map` has no possibility of failing.
+
+All of the above is explained in much more detail in the [ROP presentation/video](https://fsharpforfunandprofit.com/rop/), as well as the Rust `Result` [documentation](https://doc.rust-lang.org/std/result/index.html) and [API](https://doc.rust-lang.org/std/result/enum.Result.html).
+
+Note that we do not support procs to be used with result, only lambdas and class/singleton methods, for the reasons described above in the [Higher order functions](#higher-order-functions) section.
+
+### Message class and Messages module
+
+As shown in the examples in the ["Railway Oriented Programming" slides](https://fsharpforfunandprofit.com/rop/), we use a concept of ["Union types"](#union-types) to represent the messages passed as the `value` of a `Result` object.
+
+The `RemoteDevelopment::Messages` (plural) module, and all of its contained message classes, is an example of this sort of "Union type".
+
+Each of these message types is an instance of the `Message` class (singular). A `Message` instance is a [Value Object](#value-objects) which represents a single message to be contained as the `value` within a `Result`. It has single `context` attribute which must be of type `Hash`.
+
+All of these Messsage classes represent every possible type of success and error `Result` value which can occur within the Remote Development domain.
+
+Unlike `Result`, the `Messages` module and `Message` class are intentionally part of the `RemoteDevelopment` namespace, and are not included in the top-level `lib` directory, because they are specific to the Remote Development domain. Other domains which use `Result` may want to use their own type(s) as the `value` of a `Result`.
+
+### ROP code examples
+
+Here is an example of Railway Oriented Programming pattern, with extra code removed to focus on the patterns.
+
+First is the Services layer using `ee/lib/remote_development/workspaces/update/main.rb` as an example, which contains no logic other than calling the `Main` class in the Domain Logic layer, and converting the return value to a `ServiceResponse`:
+
+```ruby
+class UpdateService
+  attr_reader :current_user
+
+  def initialize(current_user:)
+    @current_user = current_user
+  end
+
+  def execute(workspace:, params:)
+    response_hash = Update::Main.main(workspace: workspace, current_user: current_user, params: params)
+
+    create_service_response(response_hash)
+  end
+end
+```
+
+Next, you see the `ee/lib/remote_development/workspaces/update/main.rb` class, which implements an ROP chain with two steps, `authorize` and `update`:
+
+```ruby
+class Main
+  def self.main(value)
+    initial_result = Result.ok(value)
+    result =
+      initial_result
+        .and_then(Authorizer.method(:authorize))
+        .and_then(Updater.method(:update))
+
+    case result
+    in { err: Unauthorized => message }
+      generate_error_response_from_message(message: message, reason: :unauthorized)
+    in { err: WorkspaceUpdateFailed => message }
+      generate_error_response_from_message(message: message, reason: :bad_request)
+    in { ok: WorkspaceUpdateSuccessful => message }
+      { status: :success, payload: message.context }
+    else
+      raise UnmatchedResultError.new(result: result)
+    end
+  end
+end
+```
+
+...and here is an example of the `ee/lib/remote_development/workspaces/update/updater.rb` class implementing the business logic in the "chain":
+
+```ruby
+class Updater
+  def self.update(value)
+    value => { workspace: RemoteDevelopment::Workspace => workspace, params: Hash => params }
+    if workspace.update(params)
+      Result.ok(WorkspaceUpdateSuccessful.new({ workspace: workspace }))
+    else
+      Result.err(WorkspaceUpdateFailed.new({ errors: workspace.errors }))
+    end
+  end
+end
+```
+
+## Benefits
+
+### Loose coupling, high cohesion
+
+These patterns, especially Railway Oriented Programming, allows us to split the Domain Logic layer more easily into small, loosely coupled, highly cohesive classes. This makes the individual classes and their unit tests easier to write and maintain.
+
+### Minimal logic in Service layer
+
+These patterns let all of the Service layer unit test specs be pure mock-based tests, with almost no dependencies on (or testing of) any specifics of the domain logic other than the domain classes' standard API.
+
+### More likely that you can use fast_spec_helper
+
+This loose coupling and high cohesion of the Domain Logic modules also makes it more likely that some of the Domain Logic unit tests can leverage `fast_spec_helper` to run in sub-second time, because they are not coupled to classes in the Rails monolith and their dependency graph (such as `ServiceResponse` currently is, due to its usage of `Gitlab::ErrorTracking`).
+
+### Easier for Everyone to Contribute
+
+These patterns makes the code more approachable for contributors who are less familiar with Ruby and Rails, or all of the details of our monolith.
+
+For example, if they are simply adding a feature or fixing a bug around devfile validation, they may not need to understand anything about Rails MVC, ActiveRecord, or our application structure, because the validation classes are cohesive and focused solely on validation, and deal directly with simple devfile data structures.
+
+These functional patterns are also widely known across many different programming languages and ecosystems, and thus are easier to understand than the standard Rails paradigms of inheritance and many concerns/mixins (AKA multiple inheritance) which are non-intuitive, and difficult to find/follow in the massive GitLab codebase.
+
+Also, there are currently several backend engineers on the Remote Development team who have more extensive experience in Golang than Rails. Usage of these standard patterns also allows them to contribute more easily, without having to learn as many of the nuances of Rails monolith development in order to be productive and produce clean MRs.
+
+## Differences from standard GitLab patterns
+
+### Stateless Service layer classes
+
+Some of the services do not strictly follow the [currently documented patterns for the GitLab service layer](https://docs.gitlab.com/ee/development/reusing_abstractions.html#service-classes), because in some cases those patterns don't cleanly apply to all of the Remote Development use cases.
+
+For example, the reconciliation service does not act on any specific model, or any specific user, therefore it does not have any constructor arguments.
+
+In some cases, we do still conform to the convention of passing the current_user
+in the constructor of services which do reference the user, although we may change this too in the future to pass it to `#execute` and thus make it a pure function as well.
+
+We also don't use any of the provided superclasses like BaseContainerService or its descendants, because the service contains no domain logic, and therefore these superclasses and their instance variables are not useful.
+
+If these service classes need to be changed or standardized in the future (e.g. a standardized constructor for all Service classes across the entire application), it will be straightforward to change.
+
+### 'describe #method' RSpec blocks are usually unnecessary
+
+Since much of the Domain Logic layer logic is in classes with a single singleton (class) method entry point, there is no need to have `describe .method do` blocks in specs for these classes. Omitting it saves two characters worth of indentation line length. And most of these classes and methods are named with a standard and predictable convention anyway, such as `Authorizer.authorize` and `Creator.create`.
+
+We also tend to group all base `let` fixture declarations in the top-level global describe block rather than trying to sort them out into their specific contexts, for ease of writing/maintenance/readability/consistency. Only `let` declarations which override a global one of the same name are included in a specific context.
+
+## FAQ
+
+### Why is the Result class in the top level lib directory?
+
+It it a generic reusable implementation of the Result type, and is not specific to any domain. It is intended to be reusable by any domain in the monolith which wants to use functional patterns such as Railway Oriented Programming.
+
+### What are all the `noinspection` comments in the code?
+
+Thanks for asking! See a [detailed explanation here](https://handbook.gitlab.com/handbook/tools-and-tips/editors-and-ides/jetbrains-ides/code-inspection/why-are-there-noinspection-comments/)
diff --git a/ee/lib/remote_development/message.rb b/ee/lib/remote_development/message.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d8908ab7f569b5b535267fc8b0308cbd84672ec0
--- /dev/null
+++ b/ee/lib/remote_development/message.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  # A message's context can be a hash containing any object that is relevant to the message. It will be
+  # used to provide context when the final Result from the chain is pattern matched
+  # on the message type and returned to the user.
+  # The context is required to be a hash so that it can be destructured and type-checked with
+  # rightward assignment.
+  class Message
+    attr_reader :context
+
+    # @param [Hash] context
+    # @return [Message]
+    # raise [ArgumentError] if context is not a Hash
+    def initialize(context = {})
+      raise ArgumentError, 'context must be a Hash' unless context.is_a?(Hash)
+
+      @context = context
+    end
+
+    # @param [RemoteDevelopment::Message] other
+    # @return [TrueClass, FalseClass]
+    def ==(other)
+      self.class == other.class && context == other.context
+    end
+  end
+end
diff --git a/ee/lib/remote_development/message_support.rb b/ee/lib/remote_development/message_support.rb
new file mode 100644
index 0000000000000000000000000000000000000000..091f36e616e73d8501abe5f77e8d7dc537863399
--- /dev/null
+++ b/ee/lib/remote_development/message_support.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'active_model/errors'
+
+module RemoteDevelopment
+  module MessageSupport
+    # @param [RemoteDevelopment::Message] message
+    # @param [Symbol] reason
+    # @return [Hash]
+    def generate_error_response_from_message(message:, reason:)
+      details_string =
+        case message.context
+        in {}
+          nil
+        in { details: String => error_details }
+          error_details
+        in { errors: ActiveModel::Errors => errors }
+          errors.full_messages.join(', ')
+        else
+          raise "Unexpected message context, add a case to pattern match it and convert it to a String."
+        end
+      # NOTE: Safe navigation operator is used here to prevent a type error, because Module#name is a 'String | nil'
+      message_string = message.class.name&.demodulize&.underscore&.humanize
+      error_message = details_string ? "#{message_string}: #{details_string}" : message_string
+      { status: :error, message: error_message, reason: reason }
+    end
+  end
+end
diff --git a/ee/lib/remote_development/messages.rb b/ee/lib/remote_development/messages.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5ca3cb37a299df58bc2d5d740435cd24848fc65b
--- /dev/null
+++ b/ee/lib/remote_development/messages.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  # This module contains all messages for the Remote Development domain, both errors and domain events.
+  # Note that we intentionally have not DRY'd up the declaration of the subclasses with loops and
+  # metaprogramming, because we want the types to be easily indexable and navigable within IDEs.
+  module Messages
+    #---------------------------------------------------------------
+    # Errors - message name should describe the reason for the error
+    #---------------------------------------------------------------
+
+    # Auth errors
+    Unauthorized = Class.new(Message)
+
+    # Workspace errors
+    WorkspaceUpdateFailed = Class.new(Message)
+
+    #---------------------------------------------------------
+    # Domain Events - message name should describe the outcome
+    #---------------------------------------------------------
+    WorkspaceUpdateSuccessful = Class.new(Message)
+  end
+end
diff --git a/ee/lib/remote_development/unmatched_result_error.rb b/ee/lib/remote_development/unmatched_result_error.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4a4844973d3b54aced63704913e8d1c159a2d8c
--- /dev/null
+++ b/ee/lib/remote_development/unmatched_result_error.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  class UnmatchedResultError < RuntimeError
+    # @param [Result] result
+    # @return [void]
+    def initialize(result:)
+      msg = "Failed to pattern match #{result.ok? ? "'ok'" : "'err'"} Result " \
+            "containing message of type: #{(result.ok? ? result.unwrap : result.unwrap_err).class.name}"
+
+      super(msg)
+    end
+  end
+end
diff --git a/ee/lib/remote_development/workspaces/update/authorizer.rb b/ee/lib/remote_development/workspaces/update/authorizer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba664f16075f7e25dd474b771c0e96653062dd3c
--- /dev/null
+++ b/ee/lib/remote_development/workspaces/update/authorizer.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  module Workspaces
+    module Update
+      class Authorizer
+        include Messages
+
+        # @param [Hash] value
+        # @return [Result]
+        def self.authorize(value)
+          value => { workspace: RemoteDevelopment::Workspace => workspace, current_user: User => current_user }
+
+          if current_user.can?(:update_workspace, workspace)
+            # Pass along the value to the next step
+            Result.ok(value)
+          else
+            Result.err(Unauthorized.new)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/remote_development/workspaces/update/main.rb b/ee/lib/remote_development/workspaces/update/main.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1c170b46e79f347f6508f86cbd051dd3a402ba61
--- /dev/null
+++ b/ee/lib/remote_development/workspaces/update/main.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  module Workspaces
+    module Update
+      class Main
+        include Messages
+
+        extend MessageSupport
+        private_class_method :generate_error_response_from_message
+
+        # @param [Hash] value
+        # @return [Hash]
+        # @raise [UnmatchedResultError]
+        def self.main(value)
+          initial_result = Result.ok(value)
+          result =
+            initial_result
+              .and_then(Authorizer.method(:authorize))
+              .and_then(Updater.method(:update))
+
+          case result
+          in { err: Unauthorized => message }
+            generate_error_response_from_message(message: message, reason: :unauthorized)
+          in { err: WorkspaceUpdateFailed => message }
+            generate_error_response_from_message(message: message, reason: :bad_request)
+          in { ok: WorkspaceUpdateSuccessful => message }
+            message.context => { workspace: Workspace } # Type-check the payload before returning it
+            { status: :success, payload: message.context }
+          else
+            raise UnmatchedResultError.new(result: result)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/lib/remote_development/workspaces/update/update_processor.rb b/ee/lib/remote_development/workspaces/update/update_processor.rb
deleted file mode 100644
index d1b79f9523f71ae5392d6b5c62c9946224b86e32..0000000000000000000000000000000000000000
--- a/ee/lib/remote_development/workspaces/update/update_processor.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-# frozen_string_literal: true
-
-module RemoteDevelopment
-  module Workspaces
-    module Update
-      class UpdateProcessor
-        # @param [RemoteDevelopment::Workspace] workspace
-        # @param [Hash] params
-        # @return [Array<(Hash | nil, RemoteDevelopment::Error | nil)>]
-        def process(workspace:, params:)
-          if workspace.update(params)
-            payload = { workspace: workspace }
-            [payload, nil]
-          else
-            err_msg = "Error(s) updating Workspace: #{workspace.errors.full_messages.join(', ')}"
-            error = Error.new(message: err_msg, reason: :bad_request)
-            [nil, error]
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/ee/lib/remote_development/workspaces/update/updater.rb b/ee/lib/remote_development/workspaces/update/updater.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7b7f353275d4a7da988b7d69d1c6483506b3ba4f
--- /dev/null
+++ b/ee/lib/remote_development/workspaces/update/updater.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  module Workspaces
+    module Update
+      class Updater
+        include Messages
+
+        # @param [Hash] value
+        # @return [Result]
+        def self.update(value)
+          value => { workspace: RemoteDevelopment::Workspace => workspace, params: Hash => params }
+          if workspace.update(params)
+            Result.ok(WorkspaceUpdateSuccessful.new({ workspace: workspace }))
+          else
+            Result.err(WorkspaceUpdateFailed.new({ errors: workspace.errors }))
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/message_spec.rb b/ee/spec/lib/remote_development/message_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..20df13aaff0c2fc2b21bb899e99f5d2a51745a92
--- /dev/null
+++ b/ee/spec/lib/remote_development/message_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe RemoteDevelopment::Message, feature_category: :remote_development do
+  describe '#==' do
+    it 'implements equality' do
+      expect(described_class.new({ a: 1 })).to eq(described_class.new(a: 1))
+      expect(described_class.new({ a: 1 })).not_to eq(described_class.new(a: 2))
+    end
+  end
+
+  describe 'validation' do
+    it 'requires context to be a Hash' do
+      # noinspection RubyMismatchedArgumentType - Intentionally passing wrong type to check runtime type validation
+      expect { described_class.new(1) }.to raise_error(ArgumentError, "context must be a Hash")
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/message_support_spec.rb b/ee/spec/lib/remote_development/message_support_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c2fd8003e00507e561ae26b737ea9c6b4398de8
--- /dev/null
+++ b/ee/spec/lib/remote_development/message_support_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe RemoteDevelopment::MessageSupport, feature_category: :remote_development do
+  let(:object) { Object.new.extend(described_class) }
+
+  describe '.generate_error_response_from_message' do
+    context 'for an unsupported context which is not pattern matched' do
+      let(:message) { RemoteDevelopment::Message.new(context: { unsupported: 'unmatched' }) }
+
+      it 'raises an error' do
+        expect { object.generate_error_response_from_message(message: message, reason: :does_not_matter) }
+          .to raise_error(/Unexpected message context/)
+      end
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/unmatched_result_error_spec.rb b/ee/spec/lib/remote_development/unmatched_result_error_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f0b40b47737df0990f0e7d2ba630ddc834b7c26
--- /dev/null
+++ b/ee/spec/lib/remote_development/unmatched_result_error_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe RemoteDevelopment::UnmatchedResultError, feature_category: :remote_development do
+  let(:unmatched_message_class) { stub_const('UnmatchedMessage', Class.new(RemoteDevelopment::Message)) }
+  let(:unmatched_message) { unmatched_message_class.new }
+
+  context "for an 'ok' Result" do
+    it 'has a correct message' do
+      expected_msg = "Failed to pattern match 'ok' Result containing message of type: UnmatchedMessage"
+
+      expect do
+        raise described_class.new(result: Result.ok(unmatched_message))
+      end.to raise_error(described_class, expected_msg)
+    end
+  end
+
+  context "for an 'err' Result" do
+    it 'has a correct message' do
+      expected_msg = "Failed to pattern match 'err' Result containing message of type: UnmatchedMessage"
+
+      expect do
+        raise described_class.new(result: Result.err(unmatched_message))
+      end.to raise_error(described_class, expected_msg)
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/workspaces/update/authorizer_spec.rb b/ee/spec/lib/remote_development/workspaces/update/authorizer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2317381cc9a502aa9e806d2d1177782f97e0fb15
--- /dev/null
+++ b/ee/spec/lib/remote_development/workspaces/update/authorizer_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RemoteDevelopment::Workspaces::Update::Authorizer, feature_category: :remote_development do
+  include ResultMatchers
+
+  let(:workspace) { build_stubbed(:workspace) }
+  let(:user) { build_stubbed(:user) }
+  let(:user_can_update_workspace) { true }
+  let(:params) { instance_double(Hash) }
+  let(:value) { { workspace: workspace, current_user: user, params: params } }
+
+  subject(:result) do
+    described_class.authorize(value)
+  end
+
+  before do
+    allow(user).to receive(:can?).with(:update_workspace, workspace).and_return(user_can_update_workspace)
+  end
+
+  context 'when user is authorized' do
+    it 'returns an ok Result containing the original value which was passed' do
+      expect(result).to eq(Result.ok(value))
+    end
+  end
+
+  context 'when user is not authorized' do
+    let(:user_can_update_workspace) { false }
+
+    it 'returns an err Result containing an unauthorized message with an empty context' do
+      expect(result).to be_err_result(RemoteDevelopment::Messages::Unauthorized.new)
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/workspaces/update/main_integration_spec.rb b/ee/spec/lib/remote_development/workspaces/update/main_integration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..250d306046ff3b72a453798dacfc32e4c0e89267
--- /dev/null
+++ b/ee/spec/lib/remote_development/workspaces/update/main_integration_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::RemoteDevelopment::Workspaces::Update::Main, "Integration", feature_category: :remote_development do
+  let_it_be(:user) { create(:user) }
+  let_it_be(:current_user) { user }
+  let_it_be(:workspace, refind: true) do
+    create(:workspace, user: user, desired_state: RemoteDevelopment::Workspaces::States::RUNNING)
+  end
+
+  let(:new_desired_state) { RemoteDevelopment::Workspaces::States::STOPPED }
+  let(:params) { { desired_state: new_desired_state } }
+  let(:value) { { workspace: workspace, current_user: current_user, params: params } }
+
+  subject(:response) do
+    described_class.main(value)
+  end
+
+  context 'when workspace update is successful' do
+    it 'updates the workspace and returns success' do
+      expect { subject }.to change { workspace.reload.desired_state }.to(new_desired_state)
+
+      expect(response).to eq({
+        status: :success,
+        payload: { workspace: workspace }
+      })
+    end
+  end
+
+  context 'when workspace update fails' do
+    let(:new_desired_state) { 'InvalidDesiredState' }
+
+    it 'does not update the workspace and returns error' do
+      expect { subject }.not_to change { workspace.reload }
+
+      expect(response).to eq({
+        status: :error,
+        message: "Workspace update failed: Desired state is not included in the list",
+        reason: :bad_request
+      })
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/workspaces/update/main_spec.rb b/ee/spec/lib/remote_development/workspaces/update/main_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bbcdff1fddd085ecf68ca505597418e57c05384d
--- /dev/null
+++ b/ee/spec/lib/remote_development/workspaces/update/main_spec.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RemoteDevelopment::Workspaces::Update::Main, feature_category: :remote_development do
+  include RemoteDevelopment::RailwayOrientedProgrammingHelpers
+
+  let(:initial_value) { {} }
+  let(:error_details) { 'some error details' }
+  let(:err_message_context) { { details: error_details } }
+
+  # Classes
+
+  let(:authorizer_class) { RemoteDevelopment::Workspaces::Update::Authorizer }
+  let(:updater_class) { RemoteDevelopment::Workspaces::Update::Updater }
+
+  # Methods
+
+  let(:authorizer_method) { authorizer_class.singleton_method(:authorize) }
+  let(:updater_method) { updater_class.singleton_method(:update) }
+
+  # Subject
+
+  subject(:response) { described_class.main(initial_value) }
+
+  before do
+    allow(authorizer_class).to receive(:method) { authorizer_method }
+    allow(updater_class).to receive(:method) { updater_method }
+  end
+
+  context 'when the Authorizer returns an err Result' do
+    let(:err_message_context) { {} }
+
+    before do
+      stub_methods_to_return_err_result(
+        method: authorizer_method,
+        message_class: RemoteDevelopment::Messages::Unauthorized
+      )
+    end
+
+    it 'returns an unauthorized error response' do
+      expect(response).to eq({ status: :error, message: 'Unauthorized', reason: :unauthorized })
+    end
+  end
+
+  context 'when the Updater returns an err Result' do
+    let(:errors) { ActiveModel::Errors.new(:base) }
+    let(:err_message_context) { { errors: errors } }
+
+    before do
+      stub_methods_to_return_ok_result(
+        authorizer_method
+      )
+      stub_methods_to_return_err_result(
+        method: updater_method,
+        message_class: RemoteDevelopment::Messages::WorkspaceUpdateFailed
+      )
+
+      errors.add(:base, 'err1')
+      errors.add(:base, 'err2')
+    end
+
+    it 'returns a workspace update failed error response' do
+      expect(response).to eq({
+        status: :error,
+        message: "Workspace update failed: err1, err2",
+        reason: :bad_request
+      })
+    end
+  end
+
+  context 'when the Updater returns an ok Result' do
+    let(:workspace) { build_stubbed(:workspace) }
+
+    before do
+      stub_methods_to_return_ok_result(
+        authorizer_method
+      )
+      allow(updater_method).to receive(:call).with(initial_value) do
+        Result.ok(RemoteDevelopment::Messages::WorkspaceUpdateSuccessful.new({ workspace: workspace }))
+      end
+    end
+
+    it 'returns a workspace update success response with the workspace as the payload' do
+      expect(response).to eq({
+        status: :success,
+        payload: { workspace: workspace }
+      })
+    end
+  end
+
+  context 'when an invalid Result is returned' do
+    let(:workspace) { build_stubbed(:workspace) }
+
+    before do
+      stub_methods_to_return_ok_result(
+        authorizer_method
+      )
+      allow(updater_method).to receive(:call).with(initial_value) do
+        # Note that this is not pattern matched, because there's no match for a `Result.err` with this message.
+        Result.err(RemoteDevelopment::Messages::WorkspaceUpdateSuccessful.new)
+      end
+    end
+
+    it 'raises an UnmatchedResultError' do
+      expect { response }.to raise_error(RemoteDevelopment::UnmatchedResultError)
+    end
+  end
+end
diff --git a/ee/spec/lib/remote_development/workspaces/update/update_processor_spec.rb b/ee/spec/lib/remote_development/workspaces/update/update_processor_spec.rb
deleted file mode 100644
index 821c78fa40af53fd277c0551d01c3c67a25808a0..0000000000000000000000000000000000000000
--- a/ee/spec/lib/remote_development/workspaces/update/update_processor_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe ::RemoteDevelopment::Workspaces::Update::UpdateProcessor, feature_category: :remote_development do
-  subject(:results) do
-    described_class.new.process(workspace: workspace, params: params)
-  end
-
-  let_it_be(:user) { create(:user) }
-  let_it_be(:current_user) { user }
-  let_it_be(:workspace, refind: true) do
-    create(:workspace, user: user, desired_state: RemoteDevelopment::Workspaces::States::RUNNING)
-  end
-
-  let(:new_desired_state) { RemoteDevelopment::Workspaces::States::STOPPED }
-  let(:params) do
-    {
-      desired_state: new_desired_state
-    }
-  end
-
-  describe '#process' do
-    context 'when workspace update is successful' do
-      it 'updates the workspace and returns success' do
-        expect { subject }.to change { workspace.reload.desired_state }.to(new_desired_state)
-
-        payload, error = subject
-        expect(payload).to eq({ workspace: workspace })
-        expect(error).to be_nil
-      end
-    end
-
-    context 'when workspace update fails' do
-      let(:new_desired_state) { 'InvalidDesiredState' }
-
-      it 'does not update the workspace and returns error' do
-        expect { subject }.not_to change { workspace.reload }
-
-        payload, error = subject
-        expect(payload).to be_nil
-        expect(error.message).to match(/Error/)
-        expect(error.reason).to eq(:bad_request)
-      end
-    end
-  end
-end
diff --git a/ee/spec/lib/remote_development/workspaces/update/updater_spec.rb b/ee/spec/lib/remote_development/workspaces/update/updater_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3cf88d5fa58a32ad79552252b3fc82c07e1c2161
--- /dev/null
+++ b/ee/spec/lib/remote_development/workspaces/update/updater_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::RemoteDevelopment::Workspaces::Update::Updater, feature_category: :remote_development do
+  include ResultMatchers
+
+  subject(:result) do
+    described_class.update(workspace: workspace, params: params) # rubocop:disable Rails/SaveBang
+  end
+
+  let_it_be(:user) { create(:user) }
+  let_it_be(:current_user) { user }
+  let_it_be(:workspace, refind: true) do
+    create(:workspace, user: user, desired_state: RemoteDevelopment::Workspaces::States::RUNNING)
+  end
+
+  let(:new_desired_state) { RemoteDevelopment::Workspaces::States::STOPPED }
+  let(:params) do
+    {
+      desired_state: new_desired_state
+    }
+  end
+
+  context 'when workspace update is successful' do
+    it 'updates the workspace and returns ok result containing successful message with updated workspace' do
+      expect { subject }.to change { workspace.reload.desired_state }.to(new_desired_state)
+
+      expect(result)
+        .to be_ok_result(RemoteDevelopment::Messages::WorkspaceUpdateSuccessful.new({ workspace: workspace }))
+    end
+  end
+
+  context 'when workspace update fails' do
+    let(:new_desired_state) { 'InvalidDesiredState' }
+
+    it 'does not update the workspace and returns an error result containing a failed message with model errors' do
+      expect { subject }.not_to change { workspace.reload }
+
+      expect(result).to be_err_result do |message|
+        expect(message).to be_a(RemoteDevelopment::Messages::WorkspaceUpdateFailed)
+        message.context => { errors: ActiveModel::Errors => errors }
+        expect(errors.full_messages).to match([/desired state/i])
+      end
+    end
+  end
+end
diff --git a/ee/spec/models/remote_development/remote_development_agent_config_spec.rb b/ee/spec/models/remote_development/remote_development_agent_config_spec.rb
index fcc7100b9c12bac5ada5f8b969dcb663a36354a1..b119fb30e8ade695a679831441a3813df3762d6d 100644
--- a/ee/spec/models/remote_development/remote_development_agent_config_spec.rb
+++ b/ee/spec/models/remote_development/remote_development_agent_config_spec.rb
@@ -3,6 +3,7 @@
 require 'spec_helper'
 
 RSpec.describe RemoteDevelopment::RemoteDevelopmentAgentConfig, feature_category: :remote_development do
+  # noinspection RubyResolve
   let_it_be_with_reload(:agent) { create(:ee_cluster_agent, :with_remote_development_agent_config) }
 
   subject { agent.remote_development_agent_config }
diff --git a/ee/spec/services/remote_development/workspaces/update_service_spec.rb b/ee/spec/services/remote_development/workspaces/update_service_spec.rb
index 0c2cd8d61e63f8c317664d9ecc83253cce8e31c9..feaa8b2e6aa0887dc9ac63eba87b0a26b550184a 100644
--- a/ee/spec/services/remote_development/workspaces/update_service_spec.rb
+++ b/ee/spec/services/remote_development/workspaces/update_service_spec.rb
@@ -4,53 +4,36 @@
 
 RSpec.describe RemoteDevelopment::Workspaces::UpdateService, feature_category: :remote_development do
   let(:workspace) { build_stubbed(:workspace) }
-  let(:user) { instance_double(User, can?: true) }
+  let(:user) { instance_double(User) }
   let(:params) { instance_double(Hash) }
-  let(:process_args) { { workspace: workspace, params: params } }
 
-  subject do
-    described_class.new(current_user: user).execute(workspace: workspace, params: params)
-  end
-
-  context 'when create is successful' do
-    let(:payload) { instance_double(Hash) }
+  describe '#execute' do
+    subject(:service_response) do
+      described_class.new(current_user: user).execute(workspace: workspace, params: params)
+    end
 
-    it 'returns a success ServiceResponse' do
-      allow_next_instance_of(RemoteDevelopment::Workspaces::Update::UpdateProcessor) do |processor|
-        expect(processor).to receive(:process).with(process_args).and_return([payload, nil])
-      end
-      expect(subject).to be_a(ServiceResponse)
-      expect(subject.payload).to eq(payload)
-      expect(subject.message).to be_nil
+    before do
+      allow(RemoteDevelopment::Workspaces::Update::Main)
+        .to receive(:main).with(workspace: workspace, current_user: user, params: params).and_return(response_hash)
     end
-  end
 
-  context 'when user is not authorized' do
-    let(:user) { instance_double(User, can?: false) }
+    context 'when success' do
+      let(:response_hash) { { status: :success, payload: { workspace: workspace } } }
 
-    it 'returns an error ServiceResponse' do
-      # noinspection RubyResolve
-      expect(subject).to be_error
-      expect(subject.payload).to eq({})
-      expect(subject.message).to eq('Unauthorized')
-      expect(subject.reason).to eq(:unauthorized)
+      it 'returns a success ServiceResponse' do
+        expect(service_response).to be_success
+        expect(service_response.payload.fetch(:workspace)).to eq(workspace)
+      end
     end
-  end
 
-  context 'when create fails' do
-    let(:message) { 'error message' }
-    let(:reason) { :bad_request }
-    let(:error) { RemoteDevelopment::Error.new(message: message, reason: reason) }
-
-    context 'when authorized' do
-      it 'returns an error ServiceResponse' do
-        allow_next_instance_of(RemoteDevelopment::Workspaces::Update::UpdateProcessor) do |processor|
-          expect(processor).to receive(:process).with(process_args).and_return([nil, error])
-        end
-        expect(subject).to be_a(ServiceResponse)
-        expect(subject.payload).to eq({}) # NOTE: A nil payload gets turned into an empty hash
-        expect(subject.message).to eq(message)
-        expect(subject.reason).to eq(reason)
+    context 'when error' do
+      let(:response_hash) { { status: :error, message: 'error', reason: :bad_request } }
+
+      it 'returns an error success ServiceResponse' do
+        expect(service_response).to be_error
+        service_response => { message:, reason: }
+        expect(message).to eq('error')
+        expect(reason).to eq(:bad_request)
       end
     end
   end
diff --git a/ee/spec/support/helpers/remote_development/railway_oriented_programming_helpers.rb b/ee/spec/support/helpers/remote_development/railway_oriented_programming_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c359126b8d657d75d49141956f7ea0ec7a5ea424
--- /dev/null
+++ b/ee/spec/support/helpers/remote_development/railway_oriented_programming_helpers.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module RemoteDevelopment
+  module RailwayOrientedProgrammingHelpers
+    # NOTE: Depends upon `initial_value` being defined in the including spec
+    def stub_methods_to_return_ok_result(*methods)
+      methods.each do |method|
+        allow(method).to receive(:call).with(initial_value) { Result.ok(initial_value) }
+      end
+    end
+
+    # NOTE: Depends upon `initial_value` and `err_message_context` being defined in the including spec
+    def stub_methods_to_return_err_result(method:, message_class:)
+      allow(method).to receive(:call).with(initial_value) do
+        # noinspection RubyResolve
+        Result.err(message_class.new(err_message_context))
+      end
+    end
+
+    def stub_methods_to_return_value(*methods)
+      methods.each do |method|
+        allow(method).to receive(:call).with(initial_value) { initial_value }
+      end
+    end
+  end
+end
diff --git a/lib/result.rb b/lib/result.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e72b3f13cb3bc25cab86172574e0aff37dba5ea
--- /dev/null
+++ b/lib/result.rb
@@ -0,0 +1,209 @@
+# frozen_string_literal: true
+
+# A (partial) implementation of the functional Result type, with naming conventions based on the
+# Rust implementation (https://doc.rust-lang.org/std/result/index.html)
+#
+# Modern Ruby 3+ destructuring and pattern matching are supported.
+#
+# - See "Railway Oriented Programming and the Result Class" in `ee/lib/remote_development/README.md` for details
+#   and example usage.
+# - See `spec/lib/result_spec.rb` for detailed executable example usage.
+# - See https://en.wikipedia.org/wiki/Result_type for a general description of the Result pattern.
+# - See https://fsharpforfunandprofit.com/rop/ for how this can be used with Railway Oriented Programming (ROP)
+#   to improve design and architecture
+# - See https://doc.rust-lang.org/std/result/ for the Rust implementation.
+
+# NOTE: This class is intentionally not namespaced to allow for more concise, readable, and explicit usage.
+#       It it a generic reusable implementation of the Result type, and is not specific to any domain
+# rubocop:disable Gitlab/NamespacedClass
+class Result
+  # The .ok and .err factory class methods are the only way to create a Result
+  #
+  # "self.ok" corresponds to Ok(T) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Ok
+  #
+  # @param [Object, #new] ok_value
+  # @return [Result]
+  def self.ok(ok_value)
+    new(ok_value: ok_value)
+  end
+
+  # "self.err" corresponds to Err(E) in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#variant.Err
+  #
+  # @param [Object, #new] ok_value
+  # @return [Result]
+  def self.err(err_value)
+    new(err_value: err_value)
+  end
+
+  # "#unwrap" corresponds to "unwrap" in Rust.
+  #
+  # @return [Object]
+  # @raise [RuntimeError] if called on an "err" Result
+  def unwrap
+    ok? ? value : raise("Called Result#unwrap on an 'err' Result")
+  end
+
+  # "#unwrap" corresponds to "unwrap" in Rust.
+  #
+  # @return [Object]
+  # @raise [RuntimeError] if called on an "ok" Result
+  def unwrap_err
+    err? ? value : raise("Called Result#unwrap_err on an 'ok' Result")
+  end
+
+  # The `ok?` attribute will be true if the Result was constructed with .ok, and false if it was constructed with .err
+  #
+  # "#ok?" corresponds to "is_ok" in Rust.
+  # @return [Boolean]
+  def ok?
+    # We don't make `@ok` an attr_reader, because we don't want to confusingly shadow the class method `.ok`
+    @ok
+  end
+
+  # The `err?` attribute will be false if the Result was constructed with .ok, and true if it was constructed with .err
+  # "#err?" corresponds to "is_err" in Rust.
+  #
+  # @return [Boolean]
+  def err?
+    !ok?
+  end
+
+  # `and_then` is a functional way to chain together operations which may succeed or have errors. It is passed
+  # a lambda or class (singleton) method object, and must return a Result object representing "ok"
+  # or "err".
+  #
+  # If the Result object it is called on is "ok", then the passed lambda or singleton method
+  # is called with the value contained in the Result.
+  #
+  # If the Result object it is called on is "err", then it is returned without calling the passed
+  # lambda or method.
+  #
+  # It only supports being passed a lambda, or a class (singleton) method object
+  # which responds to `call` with a single argument (arity of 1). If multiple values are needed,
+  # pass a hash or array. Note that passing `Proc` objects is NOT supported, even though the YARD
+  # annotation contains `Proc` (because the type of a lambda is also `Proc`).
+  #
+  # Passing instance methods to `and_then` is not supported, because the methods in the chain should be
+  # stateless "pure functions", and should not be persisting or referencing any instance state anyway.
+  #
+  # "#and_then" corresponds to "and_then" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then
+  #
+  # @param [Proc, Method] lambda_or_singleton_method
+  # @return [Result]
+  # @raise [TypeError]
+  def and_then(lambda_or_singleton_method)
+    validate_lambda_or_singleton_method(lambda_or_singleton_method)
+
+    # Return/passthough the Result itself if it is an err
+    return self if err?
+
+    # If the Result is ok, call the lambda or singleton method with the contained value
+    result = lambda_or_singleton_method.call(value)
+
+    unless result.is_a?(Result)
+      err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns a 'Result' type " \
+                ", but instead received '#{lambda_or_singleton_method.inspect}' which returned '#{result.class}'. " \
+                "Check that the previous method calls in the '#and_then' chain are correct."
+      raise(TypeError, err_msg)
+    end
+
+    result
+  end
+
+  # `map` is similar to `and_then`, but it is used for "single track" methods which always succeed,
+  # and have no possibility of returning an error (but they may still raise exceptions,
+  # which is unrelated to the Result handling). The passed lambda or singleton method must return
+  # a value, not a Result.
+  #
+  # If the Result object it is called on is "ok", then the passed lambda or singleton method
+  # is called with the value contained in the Result.
+  #
+  # If the Result object it is called on is "err", then it is returned without calling the passed
+  # lambda or method.
+  #
+  # "#map" corresponds to "map" in Rust: https://doc.rust-lang.org/std/result/enum.Result.html#method.map
+  #
+  # @param [Proc, Method] lambda_or_singleton_method
+  # @return [Result]
+  # @raise [TypeError]
+  def map(lambda_or_singleton_method)
+    validate_lambda_or_singleton_method(lambda_or_singleton_method)
+
+    # Return/passthrough the Result itself if it is an err
+    return self if err?
+
+    # If the Result is ok, call the lambda or singleton method with the contained value
+    mapped_value = lambda_or_singleton_method.call(value)
+
+    if mapped_value.is_a?(Result)
+      err_msg = "'Result##{__method__}' expects a lambda or singleton method object which returns an unwrapped " \
+                "value, not a 'Result', but instead received '#{lambda_or_singleton_method.inspect}' which returned " \
+                "a 'Result'."
+      raise(TypeError, err_msg)
+    end
+
+    # wrap the returned mapped_value in an "ok" Result.
+    Result.ok(mapped_value)
+  end
+
+  # `to_h` supports destructuring of a result object, for example: `result => { ok: }; puts ok`
+  #
+  # @return [Hash]
+  def to_h
+    ok? ? { ok: value } : { err: value }
+  end
+
+  # `deconstruct_keys` supports pattern matching on a Result object with a `case` statement. See specs for examples.
+  #
+  # @param [Array] keys
+  # @return [Hash]
+  # @raise [ArgumentError]
+  def deconstruct_keys(keys)
+    raise(ArgumentError, 'Use either :ok or :err for pattern matching') unless [[:ok], [:err]].include?(keys)
+
+    to_h
+  end
+
+  # @return [Boolean]
+  def ==(other)
+    # NOTE: The underlying `@ok` instance variable is a boolean, so we only need to check `ok?`, not `err?` too
+    self.class == other.class && other.ok? == ok? && other.instance_variable_get(:@value) == value
+  end
+
+  private
+
+  # The `value` attribute will contain either the ok_value or the err_value
+  attr_reader :value
+
+  def initialize(ok_value: nil, err_value: nil)
+    if (!ok_value.nil? && !err_value.nil?) || (ok_value.nil? && err_value.nil?)
+      raise(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
+    end
+
+    @ok = err_value.nil?
+    @value = ok? ? ok_value : err_value
+  end
+
+  # @param [Proc, Method] lambda_or_singleton_method
+  # @return [void]
+  # @raise [TypeError]
+  def validate_lambda_or_singleton_method(lambda_or_singleton_method)
+    is_lambda = lambda_or_singleton_method.is_a?(Proc) && lambda_or_singleton_method.lambda?
+    is_singleton_method = lambda_or_singleton_method.is_a?(Method) && lambda_or_singleton_method.owner.singleton_class?
+    unless is_lambda || is_singleton_method
+      err_msg = "'Result##{__method__}' expects a lambda or singleton method object, " \
+                "but instead received '#{lambda_or_singleton_method.inspect}'."
+      raise(TypeError, err_msg)
+    end
+
+    arity = lambda_or_singleton_method.arity
+
+    return if arity == 1
+
+    err_msg = "'Result##{__method__}' expects a lambda or singleton method object with a single argument " \
+              "(arity of 1), but instead received '#{lambda_or_singleton_method.inspect}' with an arity of #{arity}."
+    raise(ArgumentError, err_msg)
+  end
+end
+
+# rubocop:enable Gitlab/NamespacedClass
diff --git a/scripts/allowed_warnings.txt b/scripts/allowed_warnings.txt
index cc7d14c1d3c95970dd4fc6d17a41023403d84154..cb6841663480aa29b9830555a3feffdf7bd65513 100644
--- a/scripts/allowed_warnings.txt
+++ b/scripts/allowed_warnings.txt
@@ -27,3 +27,11 @@ ruby\/2\.7\.0\/net\/protocol\.rb:66: warning: previous definition of ProtocRetry
 # fine in both Ruby 2 and Ruby 3, it's unlikely it'll change again.
 # This can be removed when support for Ruby 2 is dropped.
 warning: Pattern matching is experimental, and the behavior may change in future versions of Ruby!
+
+# As of Ruby 3.1, one-line typesafe/destructuring pattern matching via "rightward assignment" has
+# been included for multiple years with no significant negative feedback or indications of removal.
+# In the event that it is removed in a future Ruby release, the changes required to fix it are
+# isolated and minor, and will have no fundamental effect on the logic. See the section
+# "Rightward assignment pattern matching and destructuring with types" in
+# ee/lib/remote_development/README.md for more information and context.
+warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby!
diff --git a/scripts/remote_development/run-smoke-test-suite.sh b/scripts/remote_development/run-smoke-test-suite.sh
index 5c1c5532a0588e1d707ec6c5bb1306e30a05e353..a48b66d31abd2aaceddf2b88acac2cc491c24ce7 100755
--- a/scripts/remote_development/run-smoke-test-suite.sh
+++ b/scripts/remote_development/run-smoke-test-suite.sh
@@ -42,6 +42,7 @@ ee/spec/finders/remote_development/workspaces_finder_spec.rb \
 ee/spec/graphql/types/query_type_spec.rb \
 ee/spec/graphql/types/remote_development/workspace_type_spec.rb \
 ee/spec/graphql/types/subscription_type_spec.rb \
+ee/spec/lib/remote_development/unmatched_result_error_spec.rb \
 ee/spec/lib/remote_development/workspaces/create/create_processor_spec.rb \
 ee/spec/lib/remote_development/workspaces/create/devfile_processor_spec.rb \
 ee/spec/lib/remote_development/workspaces/create/devfile_validator_spec.rb \
@@ -53,19 +54,24 @@ ee/spec/lib/remote_development/workspaces/reconcile/params_parser_spec.rb \
 ee/spec/lib/remote_development/workspaces/reconcile/reconcile_processor_scenarios_spec.rb \
 ee/spec/lib/remote_development/workspaces/reconcile/reconcile_processor_spec.rb \
 ee/spec/lib/remote_development/workspaces/states_spec.rb \
-ee/spec/lib/remote_development/workspaces/update/update_processor_spec.rb \
+ee/spec/lib/remote_development/workspaces/update/authorizer_spec.rb \
+ee/spec/lib/remote_development/workspaces/update/main_integration_spec.rb \
+ee/spec/lib/remote_development/workspaces/update/main_spec.rb \
+ee/spec/lib/remote_development/workspaces/update/updater_spec.rb \
 ee/spec/models/remote_development/remote_development_agent_config_spec.rb \
 ee/spec/models/remote_development/workspace_spec.rb \
 ee/spec/requests/api/graphql/mutations/remote_development/workspaces/create_spec.rb \
 ee/spec/requests/api/graphql/mutations/remote_development/workspaces/update_spec.rb \
 ee/spec/requests/api/graphql/remote_development/current_user_workspaces_spec.rb \
-ee/spec/requests/api/graphql/remote_development/workspaces_by_ids_spec.rb \
 ee/spec/requests/api/graphql/remote_development/workspace_by_id_spec.rb \
+ee/spec/requests/api/graphql/remote_development/workspaces_by_ids_spec.rb \
 ee/spec/requests/api/internal/kubernetes_spec.rb \
 ee/spec/services/remote_development/agent_config/update_service_spec.rb \
 ee/spec/services/remote_development/workspaces/create_service_spec.rb \
 ee/spec/services/remote_development/workspaces/reconcile_service_spec.rb \
 ee/spec/services/remote_development/workspaces/update_service_spec.rb \
 spec/graphql/types/subscription_type_spec.rb \
+spec/lib/result_spec.rb \
+spec/support_specs/matchers/result_matchers_spec.rb
 
 printf "\n✅✅✅ ${BGreen}All Remote Development specs passed successfully!${Color_Off} ✅✅✅\n"
diff --git a/spec/lib/result_spec.rb b/spec/lib/result_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b88521fe1419613c6f8270ceb523d8d736f5382
--- /dev/null
+++ b/spec/lib/result_spec.rb
@@ -0,0 +1,328 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+# NOTE:
+#       This spec is intended to serve as documentation examples of idiomatic usage for the `Result` type.
+#       These examples can be executed as-is in a Rails console to see the results.
+#
+#       To support this, we have intentionally used some `rubocop:disable` comments to allow for more
+#       explicit and readable examples.
+# rubocop:disable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
+RSpec.describe Result, feature_category: :remote_development do
+  describe 'usage of Result.ok and Result.err' do
+    context 'when checked with .ok? and .err?' do
+      it 'works with ok result' do
+        result = Result.ok(:success)
+        expect(result.ok?).to eq(true)
+        expect(result.err?).to eq(false)
+        expect(result.unwrap).to eq(:success)
+      end
+
+      it 'works with error result' do
+        result = Result.err(:failure)
+        expect(result.err?).to eq(true)
+        expect(result.ok?).to eq(false)
+        expect(result.unwrap_err).to eq(:failure)
+      end
+    end
+
+    context 'when checked with destructuring' do
+      it 'works with ok result' do
+        Result.ok(:success) => { ok: } # example of rightward assignment
+        expect(ok).to eq(:success)
+
+        Result.ok(:success) => { ok: success_value } # rightward assignment destructuring to different var
+        expect(success_value).to eq(:success)
+      end
+
+      it 'works with error result' do
+        Result.err(:failure) => { err: }
+        expect(err).to eq(:failure)
+
+        Result.err(:failure) => { err: error_value }
+        expect(error_value).to eq(:failure)
+      end
+    end
+
+    context 'when checked with pattern matching' do
+      def check_result_with_pattern_matching(result)
+        case result
+        in { ok: Symbol => ok_value }
+          { success: ok_value }
+        in { err: String => error_value }
+          { failure: error_value }
+        else
+          raise "Unmatched result type: #{result.unwrap.class.name}"
+        end
+      end
+
+      it 'works with ok result' do
+        ok_result = Result.ok(:success_symbol)
+        expect(check_result_with_pattern_matching(ok_result)).to eq({ success: :success_symbol })
+      end
+
+      it 'works with error result' do
+        error_result = Result.err('failure string')
+        expect(check_result_with_pattern_matching(error_result)).to eq({ failure: 'failure string' })
+      end
+
+      it 'raises error with unmatched type in pattern match' do
+        unmatched_type_result = Result.ok([])
+        expect do
+          check_result_with_pattern_matching(unmatched_type_result)
+        end.to raise_error(RuntimeError, 'Unmatched result type: Array')
+      end
+
+      it 'raises error with invalid pattern matching key' do
+        result = Result.ok(:success)
+        expect do
+          case result
+          in { invalid_pattern_match_because_it_is_not_ok_or_err: :value }
+            :unreachable_from_case
+          else
+            :unreachable_from_else
+          end
+        end.to raise_error(ArgumentError, 'Use either :ok or :err for pattern matching')
+      end
+    end
+  end
+
+  describe 'usage of #and_then' do
+    context 'when passed a proc' do
+      it 'returns last ok value in successful chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .and_then(->(value) { Result.ok(value + 1) })
+            .and_then(->(value) { Result.ok(value + 1) })
+
+        expect(final_result.ok?).to eq(true)
+        expect(final_result.unwrap).to eq(3)
+      end
+
+      it 'short-circuits the rest of the chain on the first err value encountered' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .and_then(->(value) { Result.err("invalid: #{value}") })
+            .and_then(->(value) { Result.ok(value + 1) })
+
+        expect(final_result.err?).to eq(true)
+        expect(final_result.unwrap_err).to eq('invalid: 1')
+      end
+    end
+
+    context 'when passed a module or class (singleton) method object' do
+      module MyModuleUsingResult
+        def self.double(value)
+          Result.ok(value * 2)
+        end
+
+        def self.return_err(value)
+          Result.err("invalid: #{value}")
+        end
+
+        class MyClassUsingResult
+          def self.triple(value)
+            Result.ok(value * 3)
+          end
+        end
+      end
+
+      it 'returns last ok value in successful chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .and_then(::MyModuleUsingResult.method(:double))
+            .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
+
+        expect(final_result.ok?).to eq(true)
+        expect(final_result.unwrap).to eq(6)
+      end
+
+      it 'returns first err value in failed chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .and_then(::MyModuleUsingResult.method(:double))
+            .and_then(::MyModuleUsingResult::MyClassUsingResult.method(:triple))
+            .and_then(::MyModuleUsingResult.method(:return_err))
+            .and_then(::MyModuleUsingResult.method(:double))
+
+        expect(final_result.err?).to eq(true)
+        expect(final_result.unwrap_err).to eq('invalid: 6')
+      end
+    end
+
+    describe 'type checking validation' do
+      describe 'enforcement of argument type' do
+        it 'raises TypeError if passed anything other than a lambda or singleton method object' do
+          ex = TypeError
+          msg = /expects a lambda or singleton method object/
+          # noinspection RubyMismatchedArgumentType
+          expect { Result.ok(1).and_then('string') }.to raise_error(ex, msg)
+          expect { Result.ok(1).and_then(proc { Result.ok(1) }) }.to raise_error(ex, msg)
+          expect { Result.ok(1).and_then(1.method(:to_s)) }.to raise_error(ex, msg)
+          expect { Result.ok(1).and_then(Integer.method(:to_s)) }.to raise_error(ex, msg)
+        end
+      end
+
+      describe 'enforcement of argument arity' do
+        it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
+          expect do
+            Result.ok(1).and_then(->(a, b) { Result.ok(a + b) })
+          end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
+        end
+      end
+
+      describe 'enforcement that passed lambda or method returns a Result type' do
+        it 'raises ArgumentError if passed lambda or singleton method object which returns non-Result type' do
+          expect do
+            Result.ok(1).and_then(->(a) { a + 1 })
+          end.to raise_error(TypeError, /expects .* which returns a 'Result' type/)
+        end
+      end
+    end
+  end
+
+  describe 'usage of #map' do
+    context 'when passed a proc' do
+      it 'returns last ok value in successful chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .map(->(value) { value + 1 })
+            .map(->(value) { value + 1 })
+
+        expect(final_result.ok?).to eq(true)
+        expect(final_result.unwrap).to eq(3)
+      end
+
+      it 'returns first err value in failed chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .and_then(->(value) { Result.err("invalid: #{value}") })
+            .map(->(value) { value + 1 })
+
+        expect(final_result.err?).to eq(true)
+        expect(final_result.unwrap_err).to eq('invalid: 1')
+      end
+    end
+
+    context 'when passed a module or class (singleton) method object' do
+      module MyModuleNotUsingResult
+        def self.double(value)
+          value * 2
+        end
+
+        class MyClassNotUsingResult
+          def self.triple(value)
+            value * 3
+          end
+        end
+      end
+
+      it 'returns last ok value in successful chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .map(::MyModuleNotUsingResult.method(:double))
+            .map(::MyModuleNotUsingResult::MyClassNotUsingResult.method(:triple))
+
+        expect(final_result.ok?).to eq(true)
+        expect(final_result.unwrap).to eq(6)
+      end
+
+      it 'returns first err value in failed chain' do
+        initial_result = Result.ok(1)
+        final_result =
+          initial_result
+            .map(::MyModuleNotUsingResult.method(:double))
+            .and_then(->(value) { Result.err("invalid: #{value}") })
+            .map(::MyModuleUsingResult.method(:double))
+
+        expect(final_result.err?).to eq(true)
+        expect(final_result.unwrap_err).to eq('invalid: 2')
+      end
+    end
+
+    describe 'type checking validation' do
+      describe 'enforcement of argument type' do
+        it 'raises TypeError if passed anything other than a lambda or singleton method object' do
+          ex = TypeError
+          msg = /expects a lambda or singleton method object/
+          # noinspection RubyMismatchedArgumentType
+          expect { Result.ok(1).map('string') }.to raise_error(ex, msg)
+          expect { Result.ok(1).map(proc { 1 }) }.to raise_error(ex, msg)
+          expect { Result.ok(1).map(1.method(:to_s)) }.to raise_error(ex, msg)
+          expect { Result.ok(1).map(Integer.method(:to_s)) }.to raise_error(ex, msg)
+        end
+      end
+
+      describe 'enforcement of argument arity' do
+        it 'raises ArgumentError if passed lambda or singleton method object with an arity other than 1' do
+          expect do
+            Result.ok(1).map(->(a, b) { a + b })
+          end.to raise_error(ArgumentError, /expects .* with a single argument \(arity of 1\)/)
+        end
+      end
+
+      describe 'enforcement that passed lambda or method does not return a Result type' do
+        it 'raises TypeError if passed lambda or singleton method object which returns non-Result type' do
+          expect do
+            Result.ok(1).map(->(a) { Result.ok(a + 1) })
+          end.to raise_error(TypeError, /expects .* which returns an unwrapped value, not a 'Result'/)
+        end
+      end
+    end
+  end
+
+  describe '#unwrap' do
+    it 'returns wrapped value if ok' do
+      expect(Result.ok(1).unwrap).to eq(1)
+    end
+
+    it 'raises error if err' do
+      expect { Result.err('error').unwrap }.to raise_error(RuntimeError, /called.*unwrap.*on an 'err' Result/i)
+    end
+  end
+
+  describe '#unwrap_err' do
+    it 'returns wrapped value if err' do
+      expect(Result.err('error').unwrap_err).to eq('error')
+    end
+
+    it 'raises error if ok' do
+      expect { Result.ok(1).unwrap_err }.to raise_error(RuntimeError, /called.*unwrap_err.*on an 'ok' Result/i)
+    end
+  end
+
+  describe '#==' do
+    it 'implements equality' do
+      expect(Result.ok(1)).to eq(Result.ok(1))
+      expect(Result.err('error')).to eq(Result.err('error'))
+      expect(Result.ok(1)).not_to eq(Result.ok(2))
+      expect(Result.err('error')).not_to eq(Result.err('other error'))
+      expect(Result.ok(1)).not_to eq(Result.err(1))
+    end
+  end
+
+  describe 'validation' do
+    context 'for enforcing usage of only public interface' do
+      context 'when private constructor is called with invalid params' do
+        it 'raises ArgumentError if both ok_value and err_value are passed' do
+          expect { Result.new(ok_value: :ignored, err_value: :ignored) }
+            .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
+        end
+
+        it 'raises ArgumentError if neither ok_value nor err_value are passed' do
+          expect { Result.new }
+            .to raise_error(ArgumentError, 'Do not directly use private constructor, use Result.ok or Result.err')
+        end
+      end
+    end
+  end
+end
+# rubocop:enable RSpec/DescribedClass, Lint/ConstantDefinitionInBlock, RSpec/LeakyConstantDeclaration
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
index 6171ca1a8a6f0481a4dbd38cdcab7b87439f84a6..03fcc11b6bdaa9e75613a9951e1bc7eb06cf0b5b 100644
--- a/spec/services/service_response_spec.rb
+++ b/spec/services/service_response_spec.rb
@@ -214,4 +214,17 @@
       end
     end
   end
+
+  describe '#deconstruct_keys' do
+    it 'supports pattern matching' do
+      status =
+        case described_class.error(message: 'Bad apple')
+        in { status: Symbol => status }
+          status
+        else
+          raise
+        end
+      expect(status).to eq(:error)
+    end
+  end
 end
diff --git a/spec/support/matchers/result_matchers.rb b/spec/support/matchers/result_matchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4fc2c06ba691a4fff004d3f470a32dfd68eeec10
--- /dev/null
+++ b/spec/support/matchers/result_matchers.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+# Example usage:
+#
+#   expect(Result.ok(1)).to be_ok_result(1)
+#
+#   expect(Result.err('hello')).to be_err_result do |result_value|
+#     expect(result_value).to match(/hello/i)
+#   end
+#
+# Argument to matcher is the expected value to be matched via '=='.
+# For more complex matching, pass a block to the matcher which will receive the result value as an argument.
+
+module ResultMatchers
+  def be_ok_result(expected_value = nil)
+    BeResult.new(ok_or_err: :ok, expected_value: expected_value)
+  end
+
+  def be_err_result(expected_value = nil)
+    BeResult.new(ok_or_err: :err, expected_value: expected_value)
+  end
+
+  class BeResult
+    attr_reader :ok_or_err, :actual, :failure_message_suffix, :expected_value
+
+    def initialize(ok_or_err:, expected_value:)
+      @ok_or_err = ok_or_err
+      @expected_value = expected_value
+    end
+
+    def matches?(actual, &block)
+      @actual = actual
+
+      raise "#{actual} must be a #{::Result}, but it was a #{actual.class}" unless actual.is_a?(::Result)
+
+      @failure_message_suffix = "be an '#{ok_or_err}' type"
+      return false unless actual.ok? == ok?
+
+      actual_value = actual.ok? ? actual.unwrap : actual.unwrap_err
+
+      if expected_value
+        @failure_message_suffix =
+          "have a value of #{expected_value.inspect}, but it was #{actual_value.inspect}"
+        return false unless actual_value == expected_value
+      end
+
+      # NOTE: A block can be passed to the matcher to perform more sophisticated matching,
+      #       or to provide more concise and specific failure messages.
+      block ? block.yield(actual_value) : true
+    end
+
+    def failure_message
+      "expected #{actual.inspect} to #{failure_message_suffix}"
+    end
+
+    def failure_message_when_negated
+      "expected #{actual.inspect} not to #{failure_message_suffix}"
+    end
+
+    private
+
+    def ok?
+      ok_or_err == :ok
+    end
+  end
+end
diff --git a/spec/support_specs/matchers/result_matchers_spec.rb b/spec/support_specs/matchers/result_matchers_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0c30dd08009728ba3540ad661f80f475791aea20
--- /dev/null
+++ b/spec/support_specs/matchers/result_matchers_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require_relative '../../../spec/support/matchers/result_matchers'
+
+RSpec.describe 'result matchers', feature_category: :remote_development do
+  include ResultMatchers
+
+  it 'works with value asserted via argument' do
+    expect(Result.ok(1)).to be_ok_result(1)
+    expect(Result.ok(1)).not_to be_ok_result(2)
+    expect(Result.ok(1)).not_to be_err_result(1)
+  end
+
+  it 'works with value asserted via block' do
+    expect(Result.err('hello')).to be_err_result do |result_value|
+      expect(result_value).to match(/hello/i)
+    end
+  end
+end