You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by bd...@apache.org on 2023/12/01 16:09:59 UTC

(directory-scimple) branch develop updated: Move attribute and etag logic into Repository (#411)

This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/directory-scimple.git


The following commit(s) were added to refs/heads/develop by this push:
     new 65e5f13c Move attribute and etag logic into Repository (#411)
65e5f13c is described below

commit 65e5f13c857dd031086957f20a701977b37b1734
Author: Brian Demers <bd...@apache.org>
AuthorDate: Fri Dec 1 11:09:55 2023 -0500

    Move attribute and etag logic into Repository (#411)
    
    * Move attribute and etag logic into Repository
    
    Breaks up the current `update` method into new `update` and `patch` methods.
    (Breaking change)
    
    - ETag/Version logic must be managed by Repositories, except for the case of `ServiceProviderConfiguration` (which is likely stored in memory)
      - Implementations should set `scimResource.meta.version`, how this version is defined is up to the implementation
    - The `UpdateRequest` class has been removed, the `getPatchOperations` method has been moved into a new class `PatchGenerator`
    - Current examples have been updated and demo simple in memory patch application. e.g. Get/apply patch/persist.
      - Other implementations may want generate update queries directly from the list of PatchOperations (skipping intermediate get step)
    
    * Inject PatchHandler into exmaple constructors
    
    Rename PatchHandlerImpl to DefaultPatchHandler (users of SCIMple could create different implementations)
    Update examples to inject PatchHandler
    
    * Adding section in readme that maps HTTP requests to Repository methods.
    
    NOTE: This section does not describe the supporting endpoints that SCIMple handles automatically
---
 README.md                                          |  26 +
 ...chHandlerImpl.java => DefaultPatchHandler.java} |  16 +-
 .../scim/core/repository/PatchGenerator.java       | 532 +++++++++++++++++++
 .../directory/scim/core/repository/Repository.java |  39 +-
 .../scim/core/repository/UpdateRequest.java        | 585 ---------------------
 ...ateRequestTest.java => PatchGeneratorTest.java} | 124 ++---
 .../scim/core/repository/PatchHandlerTest.java     |   4 +-
 .../jersey/service/InMemoryGroupService.java       |  23 +-
 .../jersey/service/InMemoryUserService.java        |  27 +-
 .../memory/service/InMemoryGroupService.java       |  23 +-
 .../memory/service/InMemoryUserService.java        |  26 +-
 .../quarkus/service/InMemoryGroupService.java      |  23 +-
 .../quarkus/service/InMemoryUserService.java       |  30 +-
 .../spring/service/InMemoryGroupService.java       |  23 +-
 .../spring/service/InMemoryUserService.java        |  26 +-
 .../server/rest/BaseResourceTypeResourceImpl.java  | 102 ++--
 .../scim/server/rest/BulkResourceImpl.java         |  12 +-
 .../scim/server/rest/GroupResourceImpl.java        |   6 +-
 .../scim/server/rest/UserResourceImpl.java         |   6 +-
 .../server/it/testapp/InMemoryGroupService.java    |  28 +-
 .../server/it/testapp/InMemoryUserService.java     |  26 +-
 .../scim/spring/ScimpleSpringConfiguration.java    |   8 +
 .../scim/spring/it/app/InMemoryGroupService.java   |  23 +-
 .../scim/spring/it/app/InMemoryUserService.java    |  26 +-
 24 files changed, 900 insertions(+), 864 deletions(-)

diff --git a/README.md b/README.md
index 7526bb63..14c664f5 100644
--- a/README.md
+++ b/README.md
@@ -114,6 +114,32 @@ public class InMemoryUserService implements Respository<ScimUser> {
 
 Implementing the repository interface allows the customization of create, retrieve, update, and delete methods (as well as find).  Customization is flexible - if your system implements soft deletes, create a delete method that simply sets a flag and alter the find and retrieve methods to only return "undeleted" resources.
 
+## Requests to Repository Mappings
+
+At a high level SCIMple provides the marshaling and unmarshaling of SCIM requests and responses to and from the appropriate repository methods and supporting endpoints (e.g. `/Schema`, `/ServiceProvider`, and `/ResourceTypes`).  The following table shows the mapping of SCIM requests to repository methods:
+
+| SCIM Endpoint | HTTP Method   | Repository Method       |
+|---------------|---------------|-------------------------|
+| **Users**     |               |                         |
+| /Users        | GET           | `repository.find()`     |
+| /Users        | POST          | `repository.create()`   |
+| /Users/{id}   | GET           | `repository.get()`      |
+| /Users/{id}   | PUT           | `repository.update()`   |
+| /Users/{id}   | PATCH         | `repository.patch()`    |
+| /Users/{id}   | DELETE        | `repository.delete()`   |
+| **Groups**    |               |                         |
+| /Groups       | GET           | `repository.find()`     |
+| /Groups       | POST          | `repository.create()`   |
+| /Groups/{id}  | GET           | `repository.get()`      |
+| /Groups/{id}  | PUT           | `repository.update()`   |
+| /Groups/{id}  | PATCH         | `repository.patch()`    |
+| /Groups/{id}  | DELETE        | `repository.delete()`   |
+| **Other**     |               |                         |
+| /Bulk         | POST          | `repository.*`          |
+
+> **NOTE:** The `/Bulk` endpoint enables clients to send a potentially large collection of resource operations
+in a single request, replacing the need to send multiple requests.
+
 ## Building & Contributing
 
 Clone the code and build it!
diff --git a/scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchHandlerImpl.java b/scim-core/src/main/java/org/apache/directory/scim/core/repository/DefaultPatchHandler.java
similarity index 98%
rename from scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchHandlerImpl.java
rename to scim-core/src/main/java/org/apache/directory/scim/core/repository/DefaultPatchHandler.java
index ef341e41..f03d24a9 100644
--- a/scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchHandlerImpl.java
+++ b/scim-core/src/main/java/org/apache/directory/scim/core/repository/DefaultPatchHandler.java
@@ -21,6 +21,8 @@ package org.apache.directory.scim.core.repository;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.directory.scim.core.json.ObjectMapperFactory;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
@@ -49,12 +51,13 @@ import java.util.function.Predicate;
 import static java.util.stream.Collectors.toList;
 
 /**
- * The default implementation of a PatchHandler that applies PatchOperations by walking an map equivalent
+ * The default implementation of a PatchHandler that applies PatchOperations by walking a map equivalent
  * of ScimResource.
  */
 @SuppressWarnings("unchecked")
 @Slf4j
-public class PatchHandlerImpl implements PatchHandler {
+@ApplicationScoped
+public class DefaultPatchHandler implements PatchHandler {
 
   public static final String PRIMARY = "primary";
 
@@ -70,11 +73,18 @@ public class PatchHandlerImpl implements PatchHandler {
 
   private final SchemaRegistry schemaRegistry;
 
-  public PatchHandlerImpl(SchemaRegistry schemaRegistry) {
+  @Inject
+  public DefaultPatchHandler(SchemaRegistry schemaRegistry) {
     this.schemaRegistry = schemaRegistry;
     this.objectMapper = ObjectMapperFactory.createObjectMapper(this.schemaRegistry);
   }
 
+  // For CDI
+  protected DefaultPatchHandler() {
+    this.objectMapper = null;
+    this.schemaRegistry = null;
+  }
+
   @Override
   public <T extends ScimResource> T apply(final T original, final List<PatchOperation> patchOperations) {
     if (original == null) {
diff --git a/scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchGenerator.java b/scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchGenerator.java
new file mode 100644
index 00000000..c7945cf6
--- /dev/null
+++ b/scim-core/src/main/java/org/apache/directory/scim/core/repository/PatchGenerator.java
@@ -0,0 +1,532 @@
+/*
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+ 
+* http://www.apache.org/licenses/LICENSE-2.0
+
+* Unless required by applicable law or agreed to in writing,
+* software distributed under the License is distributed on an
+* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+* KIND, either express or implied.  See the License for the
+* specific language governing permissions and limitations
+* under the License.
+*/
+
+package org.apache.directory.scim.core.repository;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.*;
+import com.flipkart.zjsonpatch.DiffFlags;
+import com.flipkart.zjsonpatch.JsonDiff;
+import org.apache.directory.scim.core.json.ObjectMapperFactory;
+import org.apache.directory.scim.core.schema.SchemaRegistry;
+import org.apache.directory.scim.spec.filter.AttributeComparisonExpression;
+import org.apache.directory.scim.spec.filter.CompareOperator;
+import org.apache.directory.scim.spec.filter.FilterExpression;
+import org.apache.directory.scim.spec.filter.ValuePathExpression;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
+import org.apache.directory.scim.spec.patch.PatchOperationPath;
+import org.apache.directory.scim.spec.resources.ScimExtension;
+import org.apache.directory.scim.spec.resources.ScimResource;
+import org.apache.directory.scim.spec.resources.TypedAttribute;
+import org.apache.directory.scim.spec.schema.AttributeContainer;
+import org.apache.directory.scim.spec.schema.Schema;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Generates a diff (i.e. a list of {@link PatchOperation}s) between two SCIM resources.
+ * This class could be used by SCIM clients when they only want to send PATCH requests over the wire (and communicate
+ * only the delta between the original and the modified resource).
+ * This class can also be used when testing SCIM servers as an easy way to generate a list of {@link PatchOperation}s.
+ */
+public class PatchGenerator {
+
+  private final SchemaRegistry schemaRegistry;
+
+  public PatchGenerator(SchemaRegistry schemaRegistry) {
+    this.schemaRegistry = schemaRegistry;
+  }
+
+  /**
+   * Returns a list of {@link PatchOperation}s that contain the differences between two {@link ScimResource}s.
+   * @param original an unmodified scim resource, as represented in a datastore.
+   * @param modified the modified version of the original scim resource.
+   * @return a list of {@link PatchOperation}s that contain the differences between two {@link ScimResource}s.
+   * @param <T> The type of the scim resource.
+   */
+  public <T extends ScimResource> List<PatchOperation> diff(T original, T modified) {
+    return new UpdateRequest<T>(original, modified, schemaRegistry).getPatchOperations();
+  }
+
+  static private class UpdateRequest<T extends ScimResource> {
+
+    private static final Logger log = LoggerFactory.getLogger(UpdateRequest.class);
+
+    private static final String OPERATION = "op";
+    private static final String PATH = "path";
+    private static final String VALUE = "value";
+
+    // Create a Jackson ObjectMapper that reads JaxB annotations
+    private final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
+
+    private final T resource;
+    private final T original;
+
+    private final Schema schema;
+
+    private final SchemaRegistry schemaRegistry;
+
+    private final Map<Schema.Attribute, Integer> addRemoveOffsetMap = new HashMap<>();
+
+    private UpdateRequest(T original, T resource, SchemaRegistry schemaRegistry) {
+      this.schemaRegistry = schemaRegistry;
+      this.original = original;
+      this.resource = resource;
+      this.schema = schemaRegistry.getSchema(original.getBaseUrn());
+    }
+
+    public List<PatchOperation> getPatchOperations() {
+      try {
+          return createPatchOperations();
+        } catch (IllegalArgumentException | IllegalAccessException | JsonProcessingException e) {
+          throw new IllegalStateException("Error creating the patch list", e);
+        }
+    }
+
+    private void sortMultiValuedCollections(Object obj1, Object obj2, AttributeContainer ac) throws IllegalArgumentException {
+      for (Schema.Attribute attribute : ac.getAttributes()) {
+        Schema.AttributeAccessor accessor = attribute.getAccessor();
+        if (attribute.isMultiValued()) {
+          @SuppressWarnings("unchecked")
+          List<Object> collection1 = obj1 != null ? (List<Object>) accessor.get(obj1) : null;
+          @SuppressWarnings("unchecked")
+          List<Object> collection2 = obj2 != null ? (List<Object>) accessor.get(obj2) : null;
+
+          Set<Object> priorities = findCommonElements(collection1, collection2);
+          PrioritySortingComparator prioritySortingComparator = new PrioritySortingComparator(priorities);
+          if (collection1 != null) {
+            Collections.sort(collection1, prioritySortingComparator);
+          }
+
+          if (collection2 != null) {
+            Collections.sort(collection2, prioritySortingComparator);
+          }
+        } else if (attribute.getType() == Schema.Attribute.Type.COMPLEX) {
+          Object nextObj1 = obj1 != null ? accessor.get(obj1) : null;
+          Object nextObj2 = obj2 != null ? accessor.get(obj2) : null;
+          sortMultiValuedCollections(nextObj1, nextObj2, attribute);
+        }
+      }
+    }
+
+    private Set<Object> findCommonElements(List<Object> list1, List<Object> list2) {
+      if (list1 == null || list2 == null) {
+        return Collections.emptySet();
+      }
+
+      Set<Object> set1 = new HashSet<>(list1);
+      Set<Object> set2 = new HashSet<>(list2);
+
+      set1 = set1.stream().map(PrioritySortingComparator::getComparableValue).collect(Collectors.toSet());
+      set2 = set2.stream().map(PrioritySortingComparator::getComparableValue).collect(Collectors.toSet());
+
+      set1.retainAll(set2);
+      return set1;
+    }
+
+    /**
+     * There is a known issue with the diffing tool that the tool will attempt to move empty arrays. By
+     * nulling out the empty arrays during comparison, this will prevent that error from occurring because
+     * deleting requires the parent node.
+     * @param node Parent node.
+     */
+    private static void nullEmptyLists(JsonNode node) {
+      List<String> objectsToDelete = new ArrayList<>();
+
+      if (node != null) {
+        Iterator<Map.Entry<String, JsonNode>> children = node.fields();
+        while(children.hasNext()) {
+          Map.Entry<String, JsonNode> child = children.next();
+          String name = child.getKey();
+          JsonNode childNode = child.getValue();
+
+          //Attempt to delete children before analyzing 
+          if (childNode.isContainerNode()) {
+            nullEmptyLists(childNode);
+          }
+
+          if (childNode instanceof ArrayNode) {
+            ArrayNode ar = (ArrayNode)childNode;
+            if (ar.size() == 0) {
+              objectsToDelete.add(name);
+            }
+          }
+        }
+
+        if (!objectsToDelete.isEmpty() && node instanceof ObjectNode) {
+          ObjectNode on = (ObjectNode)node;
+          for(String name : objectsToDelete) {
+            on.remove(name);
+          }
+        }
+      }
+    }
+
+    private List<PatchOperation> createPatchOperations() throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
+
+      sortMultiValuedCollections(this.original, this.resource, schema);
+      Map<String, ScimExtension> originalExtensions = this.original.getExtensions();
+      Map<String, ScimExtension> resourceExtensions = this.resource.getExtensions();
+      Set<String> keys = new HashSet<>();
+      keys.addAll(originalExtensions.keySet());
+      keys.addAll(resourceExtensions.keySet());
+
+      for(String key: keys) {
+        Schema extSchema = schemaRegistry.getSchema(key);
+        ScimExtension originalExtension = originalExtensions.get(key);
+        ScimExtension resourceExtension = resourceExtensions.get(key);
+        sortMultiValuedCollections(originalExtension, resourceExtension, extSchema);
+      }
+
+      JsonNode node1 = objectMapper.valueToTree(original);
+      nullEmptyLists(node1);
+      JsonNode node2 = objectMapper.valueToTree(resource);
+      nullEmptyLists(node2);
+      JsonNode differences = JsonDiff.asJson(node1, node2, DiffFlags.dontNormalizeOpIntoMoveAndCopy());
+
+      List<PatchOperation> patchOps = convertToPatchOperations(differences);
+      return patchOps;
+    }
+
+    List<PatchOperation> convertToPatchOperations(JsonNode node) throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
+      List<PatchOperation> operations = new ArrayList<>();
+      if (node == null) {
+        return Collections.emptyList();
+      }
+
+      if (!(node instanceof ArrayNode)) {
+        throw new RuntimeException("Expecting an instance of a ArrayNode, but got: " + node.getClass());
+      }
+
+      ArrayNode root = (ArrayNode) node;
+      for (int i = 0; i < root.size(); i++) {
+        ObjectNode patchNode = (ObjectNode) root.get(i);
+        JsonNode operationNode = patchNode.get(OPERATION);
+        JsonNode pathNode = patchNode.get(PATH);
+        JsonNode valueNode = patchNode.get(VALUE);
+
+        List<PatchOperation> nodeOperations = convertNodeToPatchOperations(operationNode.asText(), pathNode.asText(), valueNode);
+        if (!nodeOperations.isEmpty()) {
+          operations.addAll(nodeOperations);
+        }
+      }
+
+      return operations;
+    }
+
+    private List<PatchOperation> convertNodeToPatchOperations(String operationNode, String diffPath, JsonNode valueNode) throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
+      log.debug("convertNodeToPatchOperations: {} , {}", operationNode, diffPath);
+      List<PatchOperation> operations = new ArrayList<>();
+      PatchOperation.Type patchOpType = PatchOperation.Type.valueOf(operationNode.toUpperCase(Locale.ROOT));
+
+      if (diffPath != null && diffPath.length() >= 1) {
+        ParseData parseData = new ParseData(diffPath);
+
+        if (parseData.pathParts.isEmpty()) {
+          operations.add(handleExtensions(valueNode, patchOpType, parseData));
+        } else {
+          operations.addAll(handleAttributes(valueNode, patchOpType, parseData));
+        }
+      }
+
+      return operations;
+    }
+
+    private PatchOperation handleExtensions(JsonNode valueNode, PatchOperation.Type patchOpType, ParseData parseData) throws JsonProcessingException {
+      PatchOperation operation = new PatchOperation();
+      operation.setOperation(patchOpType);
+
+      AttributeReference attributeReference = new AttributeReference(parseData.pathUri, null);
+      PatchOperationPath patchOperationPath = new PatchOperationPath(new ValuePathExpression(attributeReference));
+
+      operation.setPath(patchOperationPath);
+      operation.setValue(determineValue(patchOpType, valueNode, parseData));
+
+      return operation;
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<PatchOperation> handleAttributes(JsonNode valueNode, PatchOperation.Type patchOpType, ParseData parseData) throws IllegalAccessException, JsonProcessingException {
+      log.trace("in handleAttributes");
+      List<PatchOperation> operations = new ArrayList<>();
+
+      List<String> attributeReferenceList = new ArrayList<>();
+      FilterExpression valueFilterExpression = null;
+      List<String> subAttributes = new ArrayList<>();
+
+      boolean processingMultiValued = false;
+      boolean processedMultiValued = false;
+      boolean done = false;
+
+      int i = 0;
+      for (String pathPart : parseData.pathParts) {
+        log.trace("path part: {}", pathPart);
+        if (done) {
+          throw new RuntimeException("Path should be done... Attribute not supported by the schema: " + pathPart);
+        } else if (processingMultiValued) {
+          parseData.traverseObjectsInArray(pathPart, patchOpType);
+
+          if (!parseData.isLastIndex(i) || patchOpType != PatchOperation.Type.ADD) {
+            if (parseData.originalObject instanceof TypedAttribute) {
+              TypedAttribute typedAttribute = (TypedAttribute) parseData.originalObject;
+              String type = typedAttribute.getType();
+              valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("type"), CompareOperator.EQ, type);
+            } else if (parseData.originalObject instanceof String || parseData.originalObject instanceof Number) {
+              String toString = parseData.originalObject.toString();
+              valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, toString);
+            } else if(parseData.originalObject instanceof Enum) {
+              Enum<?> tempEnum = (Enum<?>)parseData.originalObject;
+              valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, tempEnum.name());
+            } else {
+              if (parseData.originalObject != null) {
+                log.debug("Attribute: {} doesn't implement TypedAttribute, can't create ValueFilterExpression", parseData.originalObject.getClass());
+              } else {
+                log.debug("Attribute: null doesn't implement TypedAttribute, can't create ValueFilterExpression");
+              }
+              valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, "?");
+            }
+            processingMultiValued = false;
+            processedMultiValued = true;
+          }
+        } else {
+          Schema.Attribute attribute = parseData.ac.getAttribute(pathPart);
+
+          if (attribute != null) {
+            if (processedMultiValued) {
+              subAttributes.add(pathPart);
+            } else {
+              log.debug("Adding {} to attributeReferenceList", pathPart);
+              attributeReferenceList.add(pathPart);
+            }
+
+            parseData.traverseObjects(pathPart, attribute);
+
+            if (patchOpType == PatchOperation.Type.REPLACE &&
+              (parseData.resourceObject != null && parseData.resourceObject instanceof Collection && !((Collection<?>)parseData.resourceObject).isEmpty()) &&
+              (parseData.originalObject == null ||
+                (parseData.originalObject instanceof Collection && ((Collection<?>)parseData.originalObject).isEmpty()))) {
+              patchOpType = PatchOperation.Type.ADD;
+            }
+
+            if (attribute.isMultiValued()) {
+              processingMultiValued = true;
+            } else if (attribute.getType() != Schema.Attribute.Type.COMPLEX) {
+              done = true;
+            }
+          }
+        }
+        ++i;
+      }
+
+      if (patchOpType == PatchOperation.Type.REPLACE && (parseData.resourceObject == null ||
+        (parseData.resourceObject instanceof Collection && ((Collection<?>)parseData.resourceObject).isEmpty()))) {
+        patchOpType = PatchOperation.Type.REMOVE;
+        valueNode = null;
+      }
+
+      if (patchOpType == PatchOperation.Type.REPLACE && parseData.originalObject == null) {
+        patchOpType = PatchOperation.Type.ADD;
+      }
+
+      if (!attributeReferenceList.isEmpty()) {
+        Object value = determineValue(patchOpType, valueNode, parseData);
+
+        if (value != null && value instanceof ArrayList) {
+          List<Object> objList = (List<Object>)value;
+
+          if (!objList.isEmpty()) {
+            Object firstElement = objList.get(0);
+            if (firstElement instanceof ArrayList) {
+              objList = (List<Object>) firstElement;
+            }
+
+            for (Object obj : objList) {
+              PatchOperation operation = buildPatchOperation(patchOpType, parseData, attributeReferenceList, valueFilterExpression, subAttributes, obj);
+              if (operation != null) {
+                operations.add(operation);
+              }
+            }
+          }
+        } else {
+          PatchOperation operation = buildPatchOperation(patchOpType, parseData, attributeReferenceList, valueFilterExpression, subAttributes, value);
+          if (operation != null) {
+            operations.add(operation);
+          }
+        }
+      }
+
+      return operations;
+    }
+
+    private PatchOperation buildPatchOperation(PatchOperation.Type patchOpType, ParseData parseData, List<String> attributeReferenceList,
+                                               FilterExpression valueFilterExpression, List<String> subAttributes, Object value) {
+      PatchOperation operation = new PatchOperation();
+      operation.setOperation(patchOpType);
+      String attribute = attributeReferenceList.get(0);
+      String subAttribute = attributeReferenceList.size() > 1 ? attributeReferenceList.get(1) : null;
+
+      if (subAttribute == null && !subAttributes.isEmpty()) {
+        subAttribute = subAttributes.get(0);
+      }
+      AttributeReference attributeReference = new AttributeReference(parseData.pathUri, attribute, subAttribute);
+      PatchOperationPath patchOperationPath = new PatchOperationPath(new ValuePathExpression(attributeReference, valueFilterExpression));
+
+      operation.setPath(patchOperationPath);
+      operation.setValue(value);
+
+      return operation;
+    }
+
+    private Object determineValue(PatchOperation.Type patchOpType, JsonNode valueNode, ParseData parseData) throws JsonProcessingException {
+      if (patchOpType == PatchOperation.Type.REMOVE) {
+        return null;
+      }
+
+      if (valueNode != null) {
+        if (valueNode instanceof TextNode) {
+          return valueNode.asText();
+        } else if (valueNode instanceof BooleanNode) {
+          return valueNode.asBoolean();
+        } else if (valueNode instanceof DoubleNode || valueNode instanceof FloatNode) {
+          return valueNode.asDouble();
+        } else if (valueNode instanceof IntNode) {
+          return valueNode.asInt();
+        } else if (valueNode instanceof NullNode) {
+          return null;
+        } else if (valueNode instanceof ObjectNode) {
+          return parseData.resourceObject;
+        } else if (valueNode instanceof POJONode) {
+          POJONode pojoNode = (POJONode) valueNode;
+          return pojoNode.getPojo();
+        } else if (valueNode instanceof ArrayNode) {
+          ArrayNode arrayNode = (ArrayNode) valueNode;
+          List<Object> objectList = new ArrayList<>();
+          for(int i = 0; i < arrayNode.size(); i++) {
+            Object subObject = determineValue(patchOpType, arrayNode.get(i), parseData);
+            if (subObject != null) {
+              objectList.add(subObject);
+            }
+          }
+          return objectList;
+        }
+      }
+      return null;
+    }
+
+    private class ParseData {
+
+      List<String> pathParts;
+      Object originalObject;
+      Object resourceObject;
+      AttributeContainer ac;
+      String pathUri;
+
+      public ParseData(String diffPath) {
+        String path = diffPath.substring(1);
+        pathParts = new ArrayList<>(Arrays.asList(path.split("/")));
+
+        // Extract namespace
+        pathUri = null;
+
+        String firstPathPart = pathParts.get(0);
+        if (firstPathPart.contains(":")) {
+          pathUri = firstPathPart;
+          pathParts.remove(0);
+        }
+
+        if (pathUri != null) {
+          ac = schemaRegistry.getSchema(pathUri);
+          originalObject = original.getExtension(pathUri);
+          resourceObject = resource.getExtension(pathUri);
+        } else {
+          ac = schema;
+          originalObject = original;
+          resourceObject = resource;
+        }
+      }
+
+      public void traverseObjects(String pathPart, Schema.Attribute attribute) throws IllegalArgumentException, IllegalAccessException {
+        originalObject = lookupAttribute(originalObject, ac, pathPart);
+        resourceObject = lookupAttribute(resourceObject, ac, pathPart);
+        ac = attribute;
+      }
+
+      public void traverseObjectsInArray(String pathPart, PatchOperation.Type patchOpType) {
+        int index = Integer.parseInt(pathPart);
+
+        Schema.Attribute attr = (Schema.Attribute) ac;
+
+        Integer addRemoveOffset = addRemoveOffsetMap.getOrDefault(attr, 0);
+        switch (patchOpType) {
+          case ADD:
+            addRemoveOffsetMap.put(attr, addRemoveOffset - 1);
+            break;
+          case REMOVE:
+            addRemoveOffsetMap.put(attr, addRemoveOffset + 1);
+            break;
+          case REPLACE:
+          default:
+            // Do Nothing
+            break;
+        }
+
+        int newindex = index + addRemoveOffset;
+        if (newindex < 0) {
+          log.error("Attempting to retrieve a negative index:{} on pathPath: {}", newindex, pathPart);
+        }
+
+        originalObject = lookupIndexInArray(originalObject, newindex);
+        resourceObject = lookupIndexInArray(resourceObject, index);
+      }
+
+      public boolean isLastIndex(int index) {
+        int numPathParts = pathParts.size();
+        return index == (numPathParts - 1);
+      }
+
+      @SuppressWarnings("rawtypes")
+      private Object lookupIndexInArray(Object object, int index) {
+        if (!(object instanceof List)) {
+          throw new RuntimeException("Unsupported collection type: " + object.getClass());
+        }
+        List list = (List) object;
+        if (index >= list.size()) {
+          return null;
+        }
+
+        return list.get(index);
+      }
+
+      private Object lookupAttribute(Object object, AttributeContainer ac, String attributeName) throws IllegalArgumentException, IllegalAccessException {
+        if (object == null) {
+          return null;
+        }
+
+        Schema.Attribute attribute = ac.getAttribute(attributeName);
+        return attribute.getAccessor().get(object);
+      }
+    }
+  }
+}
diff --git a/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java b/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java
index 1e59830a..3cbbc42d 100644
--- a/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java
+++ b/scim-core/src/main/java/org/apache/directory/scim/core/repository/Repository.java
@@ -21,12 +21,15 @@ package org.apache.directory.scim.core.repository;
 
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
 import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimResource;
 
@@ -60,14 +63,38 @@ public interface Repository<T extends ScimResource> {
   /**
    * Allows the SCIM server's REST implementation to update and existing
    * resource via a PUT to a valid end-point.
-   * 
-   * @param updateRequest The ScimResource to update and persist.
+   * <br>
+   * <b>SCIM Implementation NOTE:</b> SCIM supports <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.14">versioning of resources via HTTP ETags</a>, if the (optional) {@code version} parameter is present, (and supported by the server),
+   * it can be used as a mechanism for caching and to ensure clients do not inadvertently overwrite other changes.
+   *
+   *
+   * @param id the identifier of the ScimResource to update and persist.
+   * @param version an optional version (usually used as an ETag) that can be used to optimize update requests, may be compared against, the current {@code ScimResource.meta.version}.
+   * @param resource an updated resource to persist
+   * @param includedAttributes optional set of attributes to include from ScimResource, may be used to optimize queries.
+   * @param excludedAttributes optional set of attributes to exclude from ScimResource, may be used to optimize queries.
    * @return The newly updated ScimResource.
-   * @throws ResourceException When the ScimResource cannot be
-   *         updated.
+   * @throws ResourceException When the ScimResource cannot be updated.
    */
-  T update(UpdateRequest<T> updateRequest) throws ResourceException;
-  
+  T update(String id, String version, T resource, Set<AttributeReference> includedAttributes, Set<AttributeReference> excludedAttributes) throws ResourceException;
+
+  /**
+   * Allows the SCIM server's REST implementation to update and existing
+   * resource via a PATCH to a valid end-point.
+   * <br>
+   * <b>SCIM Implementation NOTE:</b> SCIM supports <a href="https://datatracker.ietf.org/doc/html/rfc7644#section-3.14">versioning of resources via HTTP ETags</a>, if the (optional) {@code version} parameter is present, (and supported by the server),
+   * it can be used as a mechanism for caching and to ensure clients do not inadvertently overwrite other changes.
+   *
+   * @param id the identifier of the ScimResource to update and persist.
+   * @param version an optional version (usually used as an ETag) that can be used to optimize update requests, may be compared against, the current {@code ScimResource.meta.version}.
+   * @param patchOperations a list of patch operations to apply to an existing resource.
+   * @param includedAttributes optional set of attributes to include from ScimResource, may be used to optimize queries.
+   * @param excludedAttributes optional set of attributes to exclude from ScimResource, may be used to optimize queries.
+   * @return The newly updated ScimResource.
+   * @throws ResourceException When the ScimResource cannot be updated.
+   */
+  T patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributes, Set<AttributeReference> excludedAttributes) throws ResourceException;
+
   /**
    * Retrieves the ScimResource associated with the provided identifier.
    * @param id The identifier of the target ScimResource.
diff --git a/scim-core/src/main/java/org/apache/directory/scim/core/repository/UpdateRequest.java b/scim-core/src/main/java/org/apache/directory/scim/core/repository/UpdateRequest.java
deleted file mode 100644
index 52a2e245..00000000
--- a/scim-core/src/main/java/org/apache/directory/scim/core/repository/UpdateRequest.java
+++ /dev/null
@@ -1,585 +0,0 @@
-/*
-* Licensed to the Apache Software Foundation (ASF) under one
-* or more contributor license agreements.  See the NOTICE file
-* distributed with this work for additional information
-* regarding copyright ownership.  The ASF licenses this file
-* to you under the Apache License, Version 2.0 (the
-* "License"); you may not use this file except in compliance
-* with the License.  You may obtain a copy of the License at
- 
-* http://www.apache.org/licenses/LICENSE-2.0
-
-* Unless required by applicable law or agreed to in writing,
-* software distributed under the License is distributed on an
-* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-* KIND, either express or implied.  See the License for the
-* specific language governing permissions and limitations
-* under the License.
-*/
-
-package org.apache.directory.scim.core.repository;
-
-import com.fasterxml.jackson.core.JsonProcessingException;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.ArrayNode;
-import com.fasterxml.jackson.databind.node.BooleanNode;
-import com.fasterxml.jackson.databind.node.DoubleNode;
-import com.fasterxml.jackson.databind.node.FloatNode;
-import com.fasterxml.jackson.databind.node.IntNode;
-import com.fasterxml.jackson.databind.node.NullNode;
-import com.fasterxml.jackson.databind.node.ObjectNode;
-import com.fasterxml.jackson.databind.node.POJONode;
-import com.fasterxml.jackson.databind.node.TextNode;
-import com.flipkart.zjsonpatch.DiffFlags;
-import com.flipkart.zjsonpatch.JsonDiff;
-import lombok.EqualsAndHashCode;
-import lombok.Getter;
-import lombok.ToString;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.directory.scim.core.json.ObjectMapperFactory;
-import org.apache.directory.scim.core.schema.SchemaRegistry;
-import org.apache.directory.scim.spec.filter.*;
-import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
-import org.apache.directory.scim.spec.patch.PatchOperation;
-import org.apache.directory.scim.spec.patch.PatchOperation.Type;
-import org.apache.directory.scim.spec.patch.PatchOperationPath;
-import org.apache.directory.scim.spec.resources.ScimExtension;
-import org.apache.directory.scim.spec.resources.ScimResource;
-import org.apache.directory.scim.spec.resources.TypedAttribute;
-import org.apache.directory.scim.spec.schema.AttributeContainer;
-import org.apache.directory.scim.spec.schema.Schema;
-import org.apache.directory.scim.spec.schema.Schema.Attribute;
-
-import java.util.*;
-import java.util.stream.Collectors;
-
-@Slf4j
-@EqualsAndHashCode
-@ToString
-public class UpdateRequest<T extends ScimResource> {
-  
-  private static final String OPERATION = "op";
-  private static final String PATH = "path";
-  private static final String VALUE = "value";
-
-  // Create a Jackson ObjectMapper that reads JaxB annotations
-  private final ObjectMapper objectMapper = ObjectMapperFactory.getObjectMapper();
-
-  @Getter
-  private String id;
-  private T resource;
-  @Getter
-  private T original;
-  private List<PatchOperation> patchOperations;
-  private boolean initialized = false;
-
-  private Schema schema;
-
-  private PatchHandler patchHandler;
-
-  private SchemaRegistry schemaRegistry;
-
-  private Map<Attribute, Integer> addRemoveOffsetMap = new HashMap<>();
-
-  public UpdateRequest(SchemaRegistry schemaRegistry) {
-    this.schemaRegistry = schemaRegistry;
-  }
-
-  public UpdateRequest(String id, T original, T resource, SchemaRegistry schemaRegistry) {
-    this(schemaRegistry);
-    this.id = id;
-    this.original = original;
-    this.resource = resource;
-    this.patchHandler = new PatchHandlerImpl(schemaRegistry);
-    this.schema = schemaRegistry.getSchema(original.getBaseUrn());
-    initialized = true;
-  }
-
-  public UpdateRequest(String id, T original, List<PatchOperation> patchOperations, SchemaRegistry schemaRegistry) {
-    this(schemaRegistry);
-    this.id = id;
-    this.original = original;
-    this.patchOperations = patchOperations;
-    this.patchHandler = new PatchHandlerImpl(schemaRegistry);
-    this.schema = schemaRegistry.getSchema(original.getBaseUrn());
-
-    initialized = true;
-  }
-
-  public T getResource() {
-    if (!initialized) {
-      throw new IllegalStateException("UpdateRequest was not initialized");
-    }
-
-    if (resource == null) {
-      this.resource = this.patchHandler.apply(this.original, this.patchOperations);
-    }
-
-    return this.resource;
-  }
-
-  public List<PatchOperation> getPatchOperations() {
-    if (!initialized) {
-      throw new IllegalStateException("UpdateRequest was not initialized");
-    }
-    
-    if (patchOperations == null) {
-      try {
-        patchOperations = createPatchOperations(); 
-      } catch (IllegalArgumentException | IllegalAccessException | JsonProcessingException e) {
-        throw new IllegalStateException("Error creating the patch list", e);
-      }
-    }
-    
-    return patchOperations;
-  }
-
-  private void sortMultiValuedCollections(Object obj1, Object obj2, AttributeContainer ac) throws IllegalArgumentException {
-    for (Attribute attribute : ac.getAttributes()) {
-      Schema.AttributeAccessor accessor = attribute.getAccessor();
-      if (attribute.isMultiValued()) {
-        @SuppressWarnings("unchecked")
-        List<Object> collection1 = obj1 != null ? (List<Object>) accessor.get(obj1) : null;
-        @SuppressWarnings("unchecked")
-        List<Object> collection2 = obj2 != null ? (List<Object>) accessor.get(obj2) : null;
-        
-        Set<Object> priorities = findCommonElements(collection1, collection2);
-        PrioritySortingComparator prioritySortingComparator = new PrioritySortingComparator(priorities);
-        if (collection1 != null) {
-          Collections.sort(collection1, prioritySortingComparator);
-        }
-        
-        if (collection2 != null) {
-          Collections.sort(collection2, prioritySortingComparator);
-        }
-      } else if (attribute.getType() == Attribute.Type.COMPLEX) {
-        Object nextObj1 = obj1 != null ? accessor.get(obj1) : null;
-        Object nextObj2 = obj2 != null ? accessor.get(obj2) : null;
-        sortMultiValuedCollections(nextObj1, nextObj2, attribute);
-      }
-    }
-  }
-
-  private Set<Object> findCommonElements(List<Object> list1, List<Object> list2) {
-    if (list1 == null || list2 == null) {
-      return Collections.emptySet();
-    }
-    
-    Set<Object> set1 = new HashSet<>(list1);
-    Set<Object> set2 = new HashSet<>(list2);
-    
-    set1 = set1.stream().map(PrioritySortingComparator::getComparableValue).collect(Collectors.toSet());
-    set2 = set2.stream().map(PrioritySortingComparator::getComparableValue).collect(Collectors.toSet());
-    
-    set1.retainAll(set2);
-    return set1;
-  }
-
-  /**
-   * There is a know issue with the diffing tool that the tool will attempt to move empty arrays. By
-   * nulling out the empty arrays during comparison, this will prevent that error from occurring. Because
-   * deleting requires the parent node
-   * @param node Parent node.
-   */
-  private static void nullEmptyLists(JsonNode node) {
-    List<String> objectsToDelete = new ArrayList<>();
-    
-    if (node != null) {
-      Iterator<Map.Entry<String, JsonNode>> children = node.fields();
-      while(children.hasNext()) {
-        Map.Entry<String, JsonNode> child = children.next();
-        String name = child.getKey();
-        JsonNode childNode = child.getValue();
-        
-        //Attempt to delete children before analyzing 
-        if (childNode.isContainerNode()) {
-          nullEmptyLists(childNode);
-        }
-        
-        if (childNode instanceof ArrayNode) {
-          ArrayNode ar = (ArrayNode)childNode;
-          if (ar.size() == 0) {
-            objectsToDelete.add(name);
-          }
-        }
-      }
-      
-      if (!objectsToDelete.isEmpty() && node instanceof ObjectNode) {
-        ObjectNode on = (ObjectNode)node;
-        for(String name : objectsToDelete) {
-          on.remove(name);
-        }
-      }
-    }
-  }
-
-  private List<PatchOperation> createPatchOperations() throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
-
-    sortMultiValuedCollections(this.original, this.resource, schema);
-    Map<String, ScimExtension> originalExtensions = this.original.getExtensions();
-    Map<String, ScimExtension> resourceExtensions = this.resource.getExtensions();
-    Set<String> keys = new HashSet<>();
-    keys.addAll(originalExtensions.keySet());
-    keys.addAll(resourceExtensions.keySet());
-    
-    for(String key: keys) {
-      Schema extSchema = schemaRegistry.getSchema(key);
-      ScimExtension originalExtension = originalExtensions.get(key);
-      ScimExtension resourceExtension = resourceExtensions.get(key);
-      sortMultiValuedCollections(originalExtension, resourceExtension, extSchema);
-    }
-
-    JsonNode node1 = objectMapper.valueToTree(original);
-    nullEmptyLists(node1);
-    JsonNode node2 = objectMapper.valueToTree(resource);
-    nullEmptyLists(node2);
-    JsonNode differences = JsonDiff.asJson(node1, node2, DiffFlags.dontNormalizeOpIntoMoveAndCopy());
-    
-    
-    /*
-    Commenting out debug statement to prevent PII from appearing in log
-    ObjectWriter writer = objMapper.writerWithDefaultPrettyPrinter();
-    try {
-      log.debug("Original: "+writer.writeValueAsString(node1));
-      log.debug("Resource: "+writer.writeValueAsString(node2));
-    } catch (IOException e) {
-      
-    }*/
-
-    /*try {
-      log.debug("Differences: " + objMapper.writerWithDefaultPrettyPrinter().writeValueAsString(differences));
-    } catch (JsonProcessingException e) {
-      log.debug("Unable to debug differences: ", e);
-    }*/
-
-    List<PatchOperation> patchOps = convertToPatchOperations(differences);
-
-    /*try {
-      log.debug("Patch Ops: " + objMapper.writerWithDefaultPrettyPrinter().writeValueAsString(patchOps));
-    } catch (JsonProcessingException e) {
-      log.debug("Unable to debug patch ops: ", e);
-    }*/
-
-    return patchOps;
-  }
-
-  List<PatchOperation> convertToPatchOperations(JsonNode node) throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
-    List<PatchOperation> operations = new ArrayList<>();
-    if (node == null) {
-      return Collections.emptyList();
-    }
-
-    if (!(node instanceof ArrayNode)) {
-      throw new RuntimeException("Expecting an instance of a ArrayNode, but got: " + node.getClass());
-    }
-
-    ArrayNode root = (ArrayNode) node;
-    for (int i = 0; i < root.size(); i++) {
-      ObjectNode patchNode = (ObjectNode) root.get(i);
-      JsonNode operationNode = patchNode.get(OPERATION);
-      JsonNode pathNode = patchNode.get(PATH);
-      JsonNode valueNode = patchNode.get(VALUE);
-
-      List<PatchOperation> nodeOperations = convertNodeToPatchOperations(operationNode.asText(), pathNode.asText(), valueNode);
-      if (!nodeOperations.isEmpty()) {
-        operations.addAll(nodeOperations);
-      }
-    }    
-    
-    return operations;
-  }
-
-  private List<PatchOperation> convertNodeToPatchOperations(String operationNode, String diffPath, JsonNode valueNode) throws IllegalArgumentException, IllegalAccessException, JsonProcessingException {
-    log.debug("convertNodeToPatchOperations: {} , {}", operationNode, diffPath);
-    List<PatchOperation> operations = new ArrayList<>();
-    PatchOperation.Type patchOpType = PatchOperation.Type.valueOf(operationNode.toUpperCase(Locale.ROOT));
-
-    if (diffPath != null && diffPath.length() >= 1) {
-      ParseData parseData = new ParseData(diffPath);
-      
-      if (parseData.pathParts.isEmpty()) {
-        operations.add(handleExtensions(valueNode, patchOpType, parseData));
-      } else {
-        operations.addAll(handleAttributes(valueNode, patchOpType, parseData));
-      }
-    }
-    
-    return operations;
-  }
-
-  private PatchOperation handleExtensions(JsonNode valueNode, Type patchOpType, ParseData parseData) throws JsonProcessingException {
-    PatchOperation operation = new PatchOperation();
-    operation.setOperation(patchOpType);
-    
-    AttributeReference attributeReference = new AttributeReference(parseData.pathUri, null);
-    PatchOperationPath patchOperationPath = new PatchOperationPath(new ValuePathExpression(attributeReference));
-    
-    operation.setPath(patchOperationPath);
-    operation.setValue(determineValue(patchOpType, valueNode, parseData));
-    
-    return operation;
-  }
-
-  @SuppressWarnings("unchecked")
-  private List<PatchOperation> handleAttributes(JsonNode valueNode, PatchOperation.Type patchOpType, ParseData parseData) throws IllegalAccessException, JsonProcessingException {
-    log.trace("in handleAttributes");
-    List<PatchOperation> operations = new ArrayList<>();
-    
-    List<String> attributeReferenceList = new ArrayList<>();
-    FilterExpression valueFilterExpression = null;
-    List<String> subAttributes = new ArrayList<>();
-
-    boolean processingMultiValued = false;
-    boolean processedMultiValued = false;
-    boolean done = false;
-    
-    int i = 0;
-    for (String pathPart : parseData.pathParts) {
-      log.trace("path part: {}", pathPart);
-      if (done) {
-        throw new RuntimeException("Path should be done... Attribute not supported by the schema: " + pathPart);
-      } else if (processingMultiValued) {
-        parseData.traverseObjectsInArray(pathPart, patchOpType);
-
-        if (!parseData.isLastIndex(i) || patchOpType != PatchOperation.Type.ADD) {
-          if (parseData.originalObject instanceof TypedAttribute) {
-            TypedAttribute typedAttribute = (TypedAttribute) parseData.originalObject;
-            String type = typedAttribute.getType();
-            valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("type"), CompareOperator.EQ, type);
-          } else if (parseData.originalObject instanceof String || parseData.originalObject instanceof Number) {
-            String toString = parseData.originalObject.toString();
-            valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, toString);
-          } else if(parseData.originalObject instanceof Enum) {
-            Enum<?> tempEnum = (Enum<?>)parseData.originalObject;
-            valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, tempEnum.name());
-          } else {
-            if (parseData.originalObject != null) {
-              log.debug("Attribute: {} doesn't implement TypedAttribute, can't create ValueFilterExpression", parseData.originalObject.getClass());
-            } else {
-              log.debug("Attribute: null doesn't implement TypedAttribute, can't create ValueFilterExpression");
-            }
-            valueFilterExpression = new AttributeComparisonExpression(new AttributeReference("value"), CompareOperator.EQ, "?");
-          }
-          processingMultiValued = false;
-          processedMultiValued = true;
-        }
-      } else {
-        Attribute attribute = parseData.ac.getAttribute(pathPart);
-        
-        if (attribute != null) {
-          if (processedMultiValued) {
-            subAttributes.add(pathPart);
-          } else {
-            log.debug("Adding {} to attributeReferenceList", pathPart);
-            attributeReferenceList.add(pathPart);
-          }
-  
-          parseData.traverseObjects(pathPart, attribute);
-  
-          if (patchOpType == Type.REPLACE && 
-              (parseData.resourceObject != null && parseData.resourceObject instanceof Collection && !((Collection<?>)parseData.resourceObject).isEmpty()) &&
-              (parseData.originalObject == null || 
-              (parseData.originalObject instanceof Collection && ((Collection<?>)parseData.originalObject).isEmpty()))) {
-            patchOpType = Type.ADD;
-          }
-          
-          if (attribute.isMultiValued()) {
-            processingMultiValued = true;
-          } else if (attribute.getType() != Attribute.Type.COMPLEX) {
-            done = true;
-          }
-        }
-      }
-      ++i;
-    }
-    
-    if (patchOpType == Type.REPLACE && (parseData.resourceObject == null || 
-        (parseData.resourceObject instanceof Collection && ((Collection<?>)parseData.resourceObject).isEmpty()))) {
-      patchOpType = Type.REMOVE;
-      valueNode = null;
-    }
-    
-    if (patchOpType == Type.REPLACE && parseData.originalObject == null) {
-      patchOpType = Type.ADD;
-    }
-        
-    if (!attributeReferenceList.isEmpty()) {
-      Object value = determineValue(patchOpType, valueNode, parseData);
-      
-      if (value != null && value instanceof ArrayList) {
-        List<Object> objList = (List<Object>)value;
-        
-        if (!objList.isEmpty()) {
-          Object firstElement = objList.get(0); 
-          if (firstElement instanceof ArrayList) {
-            objList = (List<Object>) firstElement;
-          }
-          
-          for (Object obj : objList) {
-            PatchOperation operation = buildPatchOperation(patchOpType, parseData, attributeReferenceList, valueFilterExpression, subAttributes, obj);
-            if (operation != null) {
-              operations.add(operation);
-            }
-          }
-        }
-      } else {
-        PatchOperation operation = buildPatchOperation(patchOpType, parseData, attributeReferenceList, valueFilterExpression, subAttributes, value);
-        if (operation != null) {
-          operations.add(operation);
-        }
-      }
-    }
-    
-    return operations;
-  }
-  
-  private PatchOperation buildPatchOperation(PatchOperation.Type patchOpType, ParseData parseData, List<String> attributeReferenceList,
-                                             FilterExpression valueFilterExpression, List<String> subAttributes, Object value) {
-    PatchOperation operation = new PatchOperation();
-    operation.setOperation(patchOpType);
-    String attribute = attributeReferenceList.get(0);
-    String subAttribute = attributeReferenceList.size() > 1 ? attributeReferenceList.get(1) : null;
-
-    if (subAttribute == null && !subAttributes.isEmpty()) {
-      subAttribute = subAttributes.get(0);
-    }
-    AttributeReference attributeReference = new AttributeReference(parseData.pathUri, attribute, subAttribute);
-    PatchOperationPath patchOperationPath = new PatchOperationPath(new ValuePathExpression(attributeReference, valueFilterExpression));
-
-    operation.setPath(patchOperationPath);
-    operation.setValue(value);
-    
-    return operation;
-  }
-    
-  private Object determineValue(PatchOperation.Type patchOpType, JsonNode valueNode, ParseData parseData) throws JsonProcessingException {
-    if (patchOpType == PatchOperation.Type.REMOVE) {
-      return null;
-    }
-
-    if (valueNode != null) {
-      if (valueNode instanceof TextNode) {
-        return valueNode.asText();
-      } else if (valueNode instanceof BooleanNode) {
-        return valueNode.asBoolean();
-      } else if (valueNode instanceof DoubleNode || valueNode instanceof FloatNode) {
-        return valueNode.asDouble();
-      } else if (valueNode instanceof IntNode) {
-        return valueNode.asInt();
-      } else if (valueNode instanceof NullNode) {
-        return null;
-      } else if (valueNode instanceof ObjectNode) {
-        return parseData.resourceObject;
-      } else if (valueNode instanceof POJONode) {
-        POJONode pojoNode = (POJONode) valueNode;
-        return pojoNode.getPojo();
-      } else if (valueNode instanceof ArrayNode) {
-        ArrayNode arrayNode = (ArrayNode) valueNode;
-        List<Object> objectList = new ArrayList<>();
-        for(int i = 0; i < arrayNode.size(); i++) {
-          Object subObject = determineValue(patchOpType, arrayNode.get(i), parseData);
-          if (subObject != null) {
-            objectList.add(subObject);
-          }
-        }
-        return objectList;
-      }
-    }
-    return null;
-  }
-
-  private class ParseData {
-
-    List<String> pathParts;
-    Object originalObject;
-    Object resourceObject;
-    AttributeContainer ac;
-    String pathUri;
-
-    public ParseData(String diffPath) {
-      String path = diffPath.substring(1);
-      pathParts = new ArrayList<>(Arrays.asList(path.split("/")));
-
-      // Extract namespace
-      pathUri = null;
-
-      String firstPathPart = pathParts.get(0);
-      if (firstPathPart.contains(":")) {
-        pathUri = firstPathPart;
-        pathParts.remove(0);
-      }
-
-      if (pathUri != null) {
-        ac = schemaRegistry.getSchema(pathUri);
-        originalObject = original.getExtension(pathUri);
-        resourceObject = resource.getExtension(pathUri);
-      } else {
-        ac = schema;
-        originalObject = original;
-        resourceObject = resource;
-      }
-    }
-
-    public void traverseObjects(String pathPart, Attribute attribute) throws IllegalArgumentException, IllegalAccessException {
-      originalObject = lookupAttribute(originalObject, ac, pathPart);
-      resourceObject = lookupAttribute(resourceObject, ac, pathPart);
-      ac = attribute;
-    }
-
-    public void traverseObjectsInArray(String pathPart, Type patchOpType) {
-      int index = Integer.parseInt(pathPart);
-
-      Attribute attr = (Attribute) ac;
-      
-      Integer addRemoveOffset = addRemoveOffsetMap.getOrDefault(attr, 0);
-      switch (patchOpType) {
-      case ADD:
-        addRemoveOffsetMap.put(attr, addRemoveOffset - 1);
-        break;
-      case REMOVE:
-        addRemoveOffsetMap.put(attr, addRemoveOffset + 1);
-        break;
-      case REPLACE:
-      default:
-        // Do Nothing
-        break;
-      }
-      
-      int newindex = index + addRemoveOffset;
-      if (newindex < 0) {
-        log.error("Attempting to retrieve a negative index:{} on pathPath: {}", newindex, pathPart);
-      }
-      
-      originalObject = lookupIndexInArray(originalObject, newindex);
-      resourceObject = lookupIndexInArray(resourceObject, index);
-    }
-
-    public boolean isLastIndex(int index) {
-      int numPathParts = pathParts.size();
-      return index == (numPathParts - 1);
-    }
-
-    @SuppressWarnings("rawtypes")
-    private Object lookupIndexInArray(Object object, int index) {
-      if (!(object instanceof List)) {
-        throw new RuntimeException("Unsupported collection type: " + object.getClass());
-      }
-      List list = (List) object;
-      if (index >= list.size()) {
-        return null;
-      }
-
-      return list.get(index);
-    }
-
-    private Object lookupAttribute(Object object, AttributeContainer ac, String attributeName) throws IllegalArgumentException, IllegalAccessException {
-      if (object == null) {
-        return null;
-      }
-
-      Attribute attribute = ac.getAttribute(attributeName);
-      return attribute.getAccessor().get(object);
-    }
-  }
-}
diff --git a/scim-core/src/test/java/org/apache/directory/scim/core/repository/UpdateRequestTest.java b/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchGeneratorTest.java
similarity index 84%
rename from scim-core/src/test/java/org/apache/directory/scim/core/repository/UpdateRequestTest.java
rename to scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchGeneratorTest.java
index 3dbdfedb..babd7a38 100644
--- a/scim-core/src/test/java/org/apache/directory/scim/core/repository/UpdateRequestTest.java
+++ b/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchGeneratorTest.java
@@ -19,7 +19,7 @@
 
 package org.apache.directory.scim.core.repository;
 
-import static org.apache.directory.scim.core.repository.UpdateRequestTest.PatchOperationCondition.op;
+import static org.apache.directory.scim.core.repository.PatchGeneratorTest.PatchOperationCondition.op;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import java.util.ArrayList;
@@ -64,7 +64,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 @Slf4j
-public class UpdateRequestTest {
+public class PatchGeneratorTest {
   
   private static final String FIRST = "first";
   private static final String SECOND = "second";
@@ -85,31 +85,13 @@ public class UpdateRequestTest {
     when(schemaRegistry.getSchema(ExampleObjectExtension.URN)).thenReturn(Schemas.schemaForExtension(ExampleObjectExtension.class));
   }
 
-  @Test
-  public void testResourcePassthrough() throws Exception {
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", createUser(), createUser(), schemaRegistry);
-    ScimUser result = updateRequest.getResource();
-    log.debug("testResourcePassthrough: {}", result);
-    assertThat(result)
-              .isNotNull();
-  }
-
-  @Test
-  public void testPatchPassthrough() throws Exception {
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", createUser(), createUser1PatchOps(), schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
-    log.debug("testPatchPassthrough: {}", result);
-    assertThat(result)
-              .isNotNull();
-  }
 
   @Test
   public void testAddSingleAttribute() throws Exception {
     ScimUser user1 = createUser();
     ScimUser user2 = createUser();
     user2.setNickName("Jon");
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -123,8 +105,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.addExtension(ext);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -138,8 +119,7 @@ public class UpdateRequestTest {
     user2.getName()
          .setHonorificPrefix("Dr.");
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -155,8 +135,7 @@ public class UpdateRequestTest {
     mobilePhone.setPrimary(false);
     user2.getPhoneNumbers().add(mobilePhone);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -181,8 +160,8 @@ public class UpdateRequestTest {
     mobilePhone.setPrimary(true);
     user2.getPhoneNumbers().add(mobilePhone);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
+
     assertNotNull(operations);
     assertThat(operations).hasSize(1);
     PatchOperation operation = operations.get(0);
@@ -210,8 +189,7 @@ public class UpdateRequestTest {
     user2.getPhoneNumbers().add(homePhone);
 
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     assertNotNull(operations);
     assertEquals(2, operations.size());
     
@@ -232,8 +210,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.setActive(false);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -246,8 +223,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.getExtension(EnterpriseExtension.class).setDepartment("Dept XYZ.");
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -261,8 +237,7 @@ public class UpdateRequestTest {
     user2.getName()
          .setFamilyName("Nobody");
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -279,8 +254,7 @@ public class UpdateRequestTest {
                        .equals("work"))
          .forEach(e -> e.setValue("nobody@example.com"));
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -293,8 +267,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.setUserName(null);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -307,8 +280,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.removeExtension(EnterpriseExtension.class);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -322,9 +294,7 @@ public class UpdateRequestTest {
     user2.getName()
          .setMiddleName(null);
 
-
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -337,8 +307,7 @@ public class UpdateRequestTest {
     ScimUser user2 = createUser();
     user2.setName(null);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -356,8 +325,7 @@ public class UpdateRequestTest {
                                  .collect(Collectors.toList());
     user2.setEmails(newEmails);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -378,8 +346,7 @@ public class UpdateRequestTest {
     
     user1.getAddresses().add(localAddress);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     PatchOperation actual = assertSingleResult(result);
 
@@ -401,8 +368,7 @@ public class UpdateRequestTest {
     user2.getAddresses().add(localAddress);
     user1.getAddresses().get(0).setPostalCode("01234");
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> result = updateRequest.getPatchOperations();
+    List<PatchOperation> result = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertEquals(2, result.size());
 
@@ -422,8 +388,7 @@ public class UpdateRequestTest {
     ext2.setList(new ArrayList<>());
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     assertTrue(operations.isEmpty(), "Empty Arrays caused a diff");
   }
   
@@ -431,18 +396,19 @@ public class UpdateRequestTest {
   public void verifyEmptyArraysAreNulled() throws Exception {
     ScimUser user1 = createUser();
     ScimUser user2 = createUser();
-    
+
+    PatchGenerator patchGenerator = new PatchGenerator(schemaRegistry);
+
     //Set empty list on root object and verify no differences
     user1.setPhotos(new ArrayList<>());
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = patchGenerator.diff(user1, user2);
     assertTrue(operations.isEmpty(), "Empty Arrays are not being nulled out");
     
     //Reset user 1 and empty list on Extension and verify no differences
     user1 = createUser();
     ExampleObjectExtension ext = new ExampleObjectExtension();
     ext.setList(new ArrayList<>());
-    operations = updateRequest.getPatchOperations();
+    operations = patchGenerator.diff(user1, user2);
     assertTrue(operations.isEmpty(), "Empty Arrays are not being nulled out");
     
     //Reset extension and set empty list on element of extension then verify no differences
@@ -450,7 +416,7 @@ public class UpdateRequestTest {
     subobject.setList1(new ArrayList<>());
     ext = new ExampleObjectExtension();
     ext.setSubobject(subobject);
-    operations = updateRequest.getPatchOperations();
+    operations = patchGenerator.diff(user1, user2);
     assertTrue(operations.isEmpty(), "Empty Arrays are not being nulled out");
   }
   
@@ -476,8 +442,7 @@ public class UpdateRequestTest {
     ext2.setList(Stream.of(FIRST,SECOND).collect(Collectors.toList()));
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations)
       .hasSize(3)
@@ -507,8 +472,7 @@ public class UpdateRequestTest {
     ext2.setList(null);
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     assertNotNull(operations);
     assertEquals(2, operations.size());
     PatchOperation operation = operations.get(0);
@@ -529,8 +493,7 @@ public class UpdateRequestTest {
     ext2.setList(Stream.of(FIRST,SECOND,FOURTH).collect(Collectors.toList()));
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations)
       .hasSize(1);
@@ -551,8 +514,7 @@ public class UpdateRequestTest {
     
     user2.getName().setFormatted(nickname);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.ADD, "name.formatted", nickname)).hasSize(1);
@@ -571,8 +533,7 @@ public class UpdateRequestTest {
     user1.getName().setFormatted("");
     user2.getName().setFormatted(nickname);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.REPLACE, "name.formatted", nickname)).hasSize(1);
@@ -591,8 +552,7 @@ public class UpdateRequestTest {
     user1.getName().setFormatted("");
     user2.getName().setFormatted(nickname);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.REPLACE, "name.formatted", nickname)).hasSize(1);
@@ -612,8 +572,7 @@ public class UpdateRequestTest {
     user1.getName().setFormatted(null);
     user2.getName().setFormatted(nickname);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.ADD, "name.formatted", nickname)).hasSize(1);
@@ -632,8 +591,7 @@ public class UpdateRequestTest {
     user1.getName().setFormatted(nickname);
     user2.getName().setFormatted(null);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.REMOVE, "name.formatted")).hasSize(1);
@@ -654,8 +612,7 @@ public class UpdateRequestTest {
     ext2.setList(list2);
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     assertEquals(ops.size(), operations.size());
     for(int i = 0; i < operations.size(); i++) {
       PatchOperation actualOp = operations.get(i);
@@ -722,8 +679,7 @@ public class UpdateRequestTest {
     ext2.setList(Stream.of("A","Z").collect(Collectors.toList()));
     user2.addExtension(ext2);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     System.out.println("Number of operations: "+operations.size());
     operations.stream().forEach(op -> System.out.println(op));
 
@@ -749,8 +705,7 @@ public class UpdateRequestTest {
     user1.getName().setFormatted(nickname);
     user2.getName().setFormatted("");
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
 
     assertThat(operations).hasSize(2);
     assertThat(operations).filteredOn(op(Type.REPLACE, "name.formatted", "")).hasSize(1);
@@ -777,8 +732,7 @@ public class UpdateRequestTest {
     assertNotNull(workNumber);
     workNumber.setType(null);
 
-    UpdateRequest<ScimUser> updateRequest = new UpdateRequest<>("1234", user1, user2, schemaRegistry);
-    List<PatchOperation> operations = updateRequest.getPatchOperations();
+    List<PatchOperation> operations = new PatchGenerator(schemaRegistry).diff(user1, user2);
     assertNotNull(operations);
 
     System.out.println("Number of operations: "+operations.size());
diff --git a/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchHandlerTest.java b/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchHandlerTest.java
index 2cd31c2f..9dcafeea 100644
--- a/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchHandlerTest.java
+++ b/scim-core/src/test/java/org/apache/directory/scim/core/repository/PatchHandlerTest.java
@@ -44,12 +44,12 @@ import java.util.Optional;
 
 public class PatchHandlerTest {
 
-  PatchHandlerImpl patchHandler;
+  DefaultPatchHandler patchHandler;
 
   public PatchHandlerTest() {
     SchemaRegistry schemaRegistry = new SchemaRegistry();
     schemaRegistry.addSchema(ScimUser.class, List.of(EnterpriseExtension.class));
-    this.patchHandler = new PatchHandlerImpl(schemaRegistry);
+    this.patchHandler = new DefaultPatchHandler(schemaRegistry);
   }
 
   @Test
diff --git a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java
index 575e2add..42b09e31 100644
--- a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java
+++ b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryGroupService.java
@@ -23,15 +23,17 @@ import jakarta.annotation.PostConstruct;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 
@@ -39,6 +41,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -54,9 +57,12 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryGroupService() {}
@@ -98,9 +104,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java
index 24393fcc..2f000137 100644
--- a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java
+++ b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/service/InMemoryUserService.java
@@ -22,6 +22,7 @@ package org.apache.directory.scim.example.jersey.service;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -31,17 +32,19 @@ import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.example.jersey.extensions.LuckyNumberExtension;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.extension.EnterpriseExtension;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 
@@ -66,9 +69,12 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryUserService() {}
@@ -130,17 +136,20 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
     users.put(id, resource);
     return resource;
   }
 
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
+    users.put(id, resource);
+    return resource;
+  }
+
+
   /**
    * @see Repository#get(java.lang.String)
    */
diff --git a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java
index 599de035..f5de84a0 100644
--- a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java
+++ b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryGroupService.java
@@ -23,15 +23,17 @@ import jakarta.annotation.PostConstruct;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 
@@ -39,6 +41,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -54,9 +57,12 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryGroupService() {}
@@ -98,9 +104,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java
index 5bef2f5c..562cc662 100644
--- a/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java
+++ b/scim-server-examples/scim-server-memory/src/main/java/org/apache/directory/scim/example/memory/service/InMemoryUserService.java
@@ -22,6 +22,7 @@ package org.apache.directory.scim.example.memory.service;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -31,17 +32,19 @@ import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.example.memory.extensions.LuckyNumberExtension;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.extension.EnterpriseExtension;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 
@@ -66,9 +69,12 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryUserService() {}
@@ -130,13 +136,15 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    users.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
     users.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java
index 486d0845..af28ae7d 100644
--- a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java
+++ b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryGroupService.java
@@ -23,15 +23,17 @@ import jakarta.annotation.PostConstruct;
 import jakarta.inject.Inject;
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 
@@ -39,6 +41,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -54,9 +57,12 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryGroupService() {}
@@ -98,9 +104,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java
index 154ca57d..b8498462 100644
--- a/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java
+++ b/scim-server-examples/scim-server-quarkus/src/main/java/org/apache/directory/scim/example/quarkus/service/InMemoryUserService.java
@@ -19,10 +19,7 @@
 
 package org.apache.directory.scim.example.quarkus.service;
 
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import jakarta.annotation.PostConstruct;
@@ -31,17 +28,19 @@ import jakarta.inject.Inject;
 import jakarta.inject.Named;
 
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.example.quarkus.extensions.LuckyNumberExtension;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.extension.EnterpriseExtension;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 
@@ -66,9 +65,12 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryUserService() {}
@@ -130,13 +132,15 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    users.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
     users.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java
index 5498f6fd..0df2da7b 100644
--- a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java
+++ b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryGroupService.java
@@ -21,12 +21,14 @@ package org.apache.directory.scim.example.spring.service;
 
 import jakarta.annotation.PostConstruct;
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.*;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 
@@ -34,6 +36,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -47,8 +50,11 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private final SchemaRegistry schemaRegistry;
 
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  private final PatchHandler patchHandler;
+
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   @PostConstruct
@@ -88,9 +94,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java
index dca649c8..36ba0cc7 100644
--- a/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java
+++ b/scim-server-examples/scim-server-spring-boot/src/main/java/org/apache/directory/scim/example/spring/service/InMemoryUserService.java
@@ -22,20 +22,23 @@ package org.apache.directory.scim.example.spring.service;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
 import jakarta.annotation.PostConstruct;
 
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 import org.apache.directory.scim.example.spring.extensions.LuckyNumberExtension;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.extension.EnterpriseExtension;
 import org.apache.directory.scim.spec.filter.*;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.springframework.stereotype.Service;
 
@@ -59,8 +62,11 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private final SchemaRegistry schemaRegistry;
 
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  private final PatchHandler patchHandler;
+
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   @PostConstruct
@@ -120,13 +126,15 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    users.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
     users.put(id, resource);
     return resource;
   }
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/BaseResourceTypeResourceImpl.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/BaseResourceTypeResourceImpl.java
index 39dd8260..e16d8959 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/BaseResourceTypeResourceImpl.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/BaseResourceTypeResourceImpl.java
@@ -25,10 +25,8 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Function;
 
 import jakarta.enterprise.inject.spi.CDI;
-import jakarta.ws.rs.WebApplicationException;
 import jakarta.ws.rs.core.*;
 import jakarta.ws.rs.core.Response.ResponseBuilder;
 import jakarta.ws.rs.core.Response.Status;
@@ -40,10 +38,10 @@ import org.apache.directory.scim.core.repository.RepositoryRegistry;
 import org.apache.directory.scim.core.repository.Repository;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 import org.apache.directory.scim.spec.exception.ResourceException;
+import org.apache.directory.scim.spec.schema.Meta;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.core.repository.annotations.ScimProcessingExtension;
 import org.apache.directory.scim.core.repository.extensions.AttributeFilterExtension;
 import org.apache.directory.scim.core.repository.extensions.ProcessingExtension;
@@ -53,7 +51,6 @@ import org.apache.directory.scim.protocol.adapter.FilterWrapper;
 import org.apache.directory.scim.protocol.BaseResourceTypeResource;
 import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
 import org.apache.directory.scim.spec.filter.attribute.AttributeReferenceListWrapper;
-import org.apache.directory.scim.protocol.data.ErrorResponse;
 import org.apache.directory.scim.protocol.data.ListResponse;
 import org.apache.directory.scim.protocol.data.PatchRequest;
 import org.apache.directory.scim.protocol.data.SearchRequest;
@@ -70,14 +67,10 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
 
   private static final Logger LOG = LoggerFactory.getLogger(BaseResourceTypeResourceImpl.class);
 
-  private final SchemaRegistry schemaRegistry;
-
   private final RepositoryRegistry repositoryRegistry;
 
   private final  AttributeUtil attributeUtil;
 
-  private final  EtagGenerator etagGenerator;
-
   private final Class<T> resourceClass;
 
   // TODO: Field injection of UriInfo, Request should work with all implementations
@@ -88,10 +81,11 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
   @Context
   Request request;
 
-  public BaseResourceTypeResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry, EtagGenerator etagGenerator, Class<T> resourceClass) {
-    this.schemaRegistry = schemaRegistry;
+  @Context
+  HttpHeaders headers;
+
+  public BaseResourceTypeResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry, Class<T> resourceClass) {
     this.repositoryRegistry = repositoryRegistry;
-    this.etagGenerator = etagGenerator;
     this.resourceClass = resourceClass;
     this.attributeUtil = new AttributeUtil(schemaRegistry);
   }
@@ -126,10 +120,13 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
       }
     }
 
-    if (resource != null) {
-      EntityTag backingETag = requireEtag(resource);
-      ResponseBuilder evaluatePreconditionsResponse = request.evaluatePreconditions(backingETag);
+    if (resource == null) {
+      throw notFoundException(id);
+    }
 
+    EntityTag etag = fromVersion(resource);
+    if (etag != null) {
+      ResponseBuilder evaluatePreconditionsResponse = request.evaluatePreconditions(etag);
       if (evaluatePreconditionsResponse != null) {
         return Response.status(Status.NOT_MODIFIED).build();
       }
@@ -139,12 +136,6 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
     Set<AttributeReference> excludedAttributeReferences = AttributeReferenceListWrapper.getAttributeReferences(excludedAttributes);
     validateAttributes(attributeReferences, excludedAttributeReferences);
 
-    if (resource == null) {
-      throw notFoundException(id);
-    }
-
-    EntityTag etag = requireEtag(resource);
-
     // Process Attributes
     resource = processFilterAttributeExtensions(repository, resource, attributeReferences, excludedAttributeReferences);
     resource = attributesForDisplayThrowOnError(resource, attributeReferences, excludedAttributeReferences);
@@ -186,7 +177,7 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
 
     T created = repository.create(resource);
 
-    EntityTag etag = etag(created);
+    EntityTag etag = fromVersion(created);
 
     // Process Attributes
     created = processFilterAttributeExtensions(repository, created, attributeReferences, excludedAttributeReferences);
@@ -195,10 +186,6 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
       created = attributesForDisplay(created, attributeReferences, excludedAttributeReferences);
     } catch (AttributeException e) {
         log.debug("Exception thrown while processing attributes", e);
-        return Response.status(Status.CREATED)
-                .location(buildLocationTag(created))
-                .tag(etag)
-                .build();
     }
 
     return Response.status(Status.CREATED)
@@ -244,7 +231,6 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
       List<T> results = new ArrayList<>();
 
       for (T resource : filterResp.getResources()) {
-        requireEtag(resource);
 
         // Process Attributes
         resource = processFilterAttributeExtensions(repository, resource, attributeReferences, excludedAttributeReferences);
@@ -262,15 +248,14 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
 
   @Override
   public Response update(T resource, String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException, ResourceException {
-    return update(id, attributes, excludedAttributes, (stored) ->
-      new UpdateRequest<>(id, stored, resource, schemaRegistry));
+    return update(attributes, excludedAttributes, (etag, includeAttributes, excludeAttributes, repository)
+      -> repository.update(id, etag,resource, includeAttributes, excludeAttributes));
   }
 
   @Override
   public Response patch(PatchRequest patchRequest, String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException, ResourceException {
-
-    return update(id, attributes, excludedAttributes, (stored) ->
-      new UpdateRequest<>(id, stored, patchRequest.getPatchOperationList(), schemaRegistry));
+    return update(attributes, excludedAttributes, (etag, includeAttributes, excludeAttributes, repository)
+      -> repository.patch(id, etag, patchRequest.getPatchOperationList(), includeAttributes, excludeAttributes));
   }
 
   @Override
@@ -281,7 +266,7 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
         .build();
   }
 
-  private Response update(String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes, Function<T, UpdateRequest<T>> updateRequestFunction) throws ScimException, ResourceException {
+  private Response update(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes, UpdateFunction<T> updateFunction) throws ScimException, ResourceException {
 
     Repository<T> repository = getRepositoryInternal();
 
@@ -289,23 +274,14 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
     Set<AttributeReference> excludedAttributeReferences = AttributeReferenceListWrapper.getAttributeReferences(excludedAttributes);
     validateAttributes(attributeReferences, excludedAttributeReferences);
 
-    T stored = repository.get(id);
-
-    if (stored == null) {
-      throw notFoundException(id);
-    }
-
-    EntityTag backingETag = requireEtag(stored);
-    validatePreconditions(id, backingETag);
-
-    UpdateRequest<T> updateRequest = updateRequestFunction.apply(stored);
-    T updated = repository.update(updateRequest);
+    String requestEtag = headers.getHeaderString("ETag");
+    T updated = updateFunction.update(requestEtag, attributeReferences, excludedAttributeReferences, repository);
 
     // Process Attributes
     updated = processFilterAttributeExtensions(repository, updated, attributeReferences, excludedAttributeReferences);
     updated = attributesForDisplayIgnoreErrors(updated, attributeReferences, excludedAttributeReferences);
 
-    EntityTag etag = etag(updated);
+    EntityTag etag = fromVersion(updated);
     return Response.ok(updated)
       .location(buildLocationTag(updated))
       .tag(etag)
@@ -382,35 +358,25 @@ public abstract class BaseResourceTypeResourceImpl<T extends ScimResource> imple
     return new ScimException(Status.NOT_FOUND, "Resource " + id + " not found");
   }
 
-  private void validatePreconditions(String id, EntityTag etag) {
-    ResponseBuilder response = request.evaluatePreconditions(etag);
-    if (response != null) {
-      throw new WebApplicationException(response
-        .entity(new ErrorResponse(Status.PRECONDITION_FAILED, "Failed to update record, backing record has changed - " + id))
-        .build());
-    }
-  }
-
-  private EntityTag requireEtag(ScimResource resource) throws ScimException {
-    try {
-      return etagGenerator.generateEtag(resource);
-    } catch (EtagGenerationException e) {
-      throw new ScimException(Status.INTERNAL_SERVER_ERROR, "Failed to generate the etag", e);
+  private void validateAttributes(Set<AttributeReference> attributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ScimException {
+    if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
+      throw new ScimException(Status.BAD_REQUEST, "Cannot include both attributes and excluded attributes in a single request");
     }
   }
 
-  private EntityTag etag(ScimResource resource) {
-    try {
-      return etagGenerator.generateEtag(resource);
-    } catch (EtagGenerationException e) {
-      log.warn("Failed to generate etag for resource", e);
-      return null;
+  private EntityTag fromVersion(ScimResource resource) {
+    Meta meta = resource.getMeta();
+    if (meta != null) {
+      String version = meta.getVersion();
+      if (version != null) {
+        return new EntityTag(version);
+      }
     }
+    return null;
   }
 
-  private void validateAttributes(Set<AttributeReference> attributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ScimException {
-    if (!attributeReferences.isEmpty() && !excludedAttributeReferences.isEmpty()) {
-      throw new ScimException(Status.BAD_REQUEST, "Cannot include both attributes and excluded attributes in a single request");
-    }
+  @FunctionalInterface
+  private interface UpdateFunction<T extends ScimResource> {
+    T update(String etag, Set<AttributeReference> includeAttributes, Set<AttributeReference> excludeAttributes, Repository<T> repository) throws ResourceException;
   }
 }
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/BulkResourceImpl.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/BulkResourceImpl.java
index 4d1291a0..72c9155e 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/BulkResourceImpl.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/BulkResourceImpl.java
@@ -36,7 +36,6 @@ import org.apache.directory.scim.server.exception.UnableToRetrieveResourceExcept
 import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
 import org.apache.directory.scim.core.repository.RepositoryRegistry;
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.protocol.BulkResource;
 import org.apache.directory.scim.protocol.data.BulkOperation;
 import org.apache.directory.scim.protocol.data.BulkOperation.Method;
@@ -288,10 +287,7 @@ public class BulkResourceImpl implements BulkResource {
         Class<ScimResource> scimResourceClass = (Class<ScimResource>) scimResource.getClass();
         Repository<ScimResource> repository = repositoryRegistry.getRepository(scimResourceClass);
 
-        ScimResource original = repository.get(scimResourceId);
-
-        UpdateRequest<ScimResource> updateRequest = new UpdateRequest<>(scimResourceId, original, scimResource, schemaRegistry);
-        repository.update(updateRequest);
+        repository.update(scimResourceId, null, scimResource, Collections.emptySet(), Collections.emptySet());
       } catch (UnresolvableOperationException unresolvableOperationException) {
         log.error("Could not complete final resolution pass, unresolvable bulkId", unresolvableOperationException);
 
@@ -464,11 +460,7 @@ public class BulkResourceImpl implements BulkResource {
                                      + 1);
 
       try {
-        ScimResource original = repository.get(id);
-
-        UpdateRequest<ScimResource> updateRequest = new UpdateRequest<>(id, original, scimResource, schemaRegistry);
-        
-        repository.update(updateRequest);
+        repository.update(id, null, scimResource, Collections.emptySet(), Collections.emptySet());
         operationResult.setStatus(StatusWrapper.wrap(Status.OK));
       } catch (UnableToRetrieveResourceException e) {
         operationResult.setStatus(StatusWrapper.wrap(Status.NOT_FOUND));
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/GroupResourceImpl.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/GroupResourceImpl.java
index 1384204c..bcdeb1e1 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/GroupResourceImpl.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/GroupResourceImpl.java
@@ -35,12 +35,12 @@ import org.apache.directory.scim.core.schema.SchemaRegistry;
 public class GroupResourceImpl extends BaseResourceTypeResourceImpl<ScimGroup> implements GroupResource {
 
   @Inject
-  public GroupResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry, EtagGenerator etagGenerator) {
-    super(schemaRegistry, repositoryRegistry, etagGenerator, ScimGroup.class);
+  public GroupResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry) {
+    super(schemaRegistry, repositoryRegistry, ScimGroup.class);
   }
 
   public GroupResourceImpl() {
     // CDI
-    this(null, null, null);
+    this(null, null);
   }
 }
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/UserResourceImpl.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/UserResourceImpl.java
index 75bebe21..963f43ca 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/UserResourceImpl.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/UserResourceImpl.java
@@ -39,12 +39,12 @@ import org.apache.directory.scim.core.schema.SchemaRegistry;
 public class UserResourceImpl extends BaseResourceTypeResourceImpl<ScimUser> implements UserResource {
 
   @Inject
-  public UserResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry, EtagGenerator etagGenerator) {
-    super(schemaRegistry, repositoryRegistry, etagGenerator, ScimUser.class);
+  public UserResourceImpl(SchemaRegistry schemaRegistry, RepositoryRegistry repositoryRegistry) {
+    super(schemaRegistry, repositoryRegistry, ScimUser.class);
   }
 
   public UserResourceImpl() {
     // CDI
-    this(null, null, null);
+    this(null, null);
   }
 }
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java
index b4de6ee0..2dac3ead 100644
--- a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java
@@ -25,24 +25,22 @@ import jakarta.inject.Inject;
 import jakarta.inject.Named;
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.UUID;
+import java.util.*;
 import java.util.stream.Collectors;
 
 @Named
@@ -53,9 +51,12 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryGroupService() {}
@@ -97,9 +98,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java
index f9d29f3c..909df75b 100644
--- a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java
@@ -24,21 +24,24 @@ import jakarta.enterprise.context.ApplicationScoped;
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.FilterExpressions;
 import org.apache.directory.scim.spec.filter.FilterResponse;
 import org.apache.directory.scim.spec.filter.Filter;
 import org.apache.directory.scim.spec.filter.PageRequest;
 import org.apache.directory.scim.spec.filter.SortRequest;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -62,9 +65,12 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private SchemaRegistry schemaRegistry;
 
+  private PatchHandler patchHandler;
+
   @Inject
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   protected InMemoryUserService() {}
@@ -123,13 +129,15 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    users.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
     users.put(id, resource);
     return resource;
   }
diff --git a/support/spring-boot/src/main/java/org/apache/directory/scim/spring/ScimpleSpringConfiguration.java b/support/spring-boot/src/main/java/org/apache/directory/scim/spring/ScimpleSpringConfiguration.java
index 898c8332..a976a330 100644
--- a/support/spring-boot/src/main/java/org/apache/directory/scim/spring/ScimpleSpringConfiguration.java
+++ b/support/spring-boot/src/main/java/org/apache/directory/scim/spring/ScimpleSpringConfiguration.java
@@ -23,6 +23,8 @@ import jakarta.enterprise.inject.Instance;
 import jakarta.enterprise.util.TypeLiteral;
 import jakarta.ws.rs.core.Application;
 import org.apache.commons.lang3.NotImplementedException;
+import org.apache.directory.scim.core.repository.DefaultPatchHandler;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.core.repository.Repository;
 import org.apache.directory.scim.core.repository.RepositoryRegistry;
 import org.apache.directory.scim.core.repository.SelfIdResolver;
@@ -73,6 +75,12 @@ public class ScimpleSpringConfiguration {
     return new SchemaRegistry();
   }
 
+  @Bean
+  @ConditionalOnMissingBean
+  PatchHandler patchHandler(SchemaRegistry schemaRegistry) {
+    return new DefaultPatchHandler(schemaRegistry);
+  }
+
   @Bean
   @ConditionalOnMissingBean
   @ConditionalOnBean(SelfIdResolver.class)
diff --git a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java
index 688367f1..07a79d03 100644
--- a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java
+++ b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryGroupService.java
@@ -22,12 +22,14 @@ package org.apache.directory.scim.spring.it.app;
 import jakarta.annotation.PostConstruct;
 import jakarta.ws.rs.core.Response;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.*;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.ScimExtension;
 import org.apache.directory.scim.spec.resources.ScimGroup;
 import org.springframework.stereotype.Service;
@@ -36,6 +38,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.UUID;
 import java.util.stream.Collectors;
 
@@ -46,8 +49,11 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
 
   private final SchemaRegistry schemaRegistry;
 
-  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+  private final PatchHandler patchHandler;
+
+  public InMemoryGroupService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   @PostConstruct
@@ -87,9 +93,14 @@ public class InMemoryGroupService implements Repository<ScimGroup> {
   }
 
   @Override
-  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimGroup resource = updateRequest.getResource();
+  public ScimGroup update(String id, String version, ScimGroup resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimGroup resource = patchHandler.apply(get(id), patchOperations);
     groups.put(id, resource);
     return resource;
   }
diff --git a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java
index 51b50b64..430d837f 100644
--- a/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java
+++ b/support/spring-boot/src/test/java/org/apache/directory/scim/spring/it/app/InMemoryUserService.java
@@ -21,12 +21,14 @@ package org.apache.directory.scim.spring.it.app;
 
 import jakarta.annotation.PostConstruct;
 import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.core.repository.PatchHandler;
 import org.apache.directory.scim.core.repository.Repository;
-import org.apache.directory.scim.core.repository.UpdateRequest;
 import org.apache.directory.scim.core.schema.SchemaRegistry;
 import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
-import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.spec.exception.ResourceException;
 import org.apache.directory.scim.spec.filter.*;
+import org.apache.directory.scim.spec.filter.attribute.AttributeReference;
+import org.apache.directory.scim.spec.patch.PatchOperation;
 import org.apache.directory.scim.spec.resources.*;
 import org.springframework.stereotype.Service;
 
@@ -34,6 +36,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -56,8 +59,11 @@ public class InMemoryUserService implements Repository<ScimUser> {
 
   private final SchemaRegistry schemaRegistry;
 
-  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+  private final PatchHandler patchHandler;
+
+  public InMemoryUserService(SchemaRegistry schemaRegistry, PatchHandler patchHandler) {
     this.schemaRegistry = schemaRegistry;
+    this.patchHandler = patchHandler;
   }
 
   @PostConstruct
@@ -117,13 +123,15 @@ public class InMemoryUserService implements Repository<ScimUser> {
     return resource;
   }
 
-  /**
-   * @see Repository#update(UpdateRequest)
-   */
   @Override
-  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
-    String id = updateRequest.getId();
-    ScimUser resource = updateRequest.getResource();
+  public ScimUser update(String id, String version, ScimUser resource, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    users.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimUser patch(String id, String version, List<PatchOperation> patchOperations, Set<AttributeReference> includedAttributeReferences, Set<AttributeReference> excludedAttributeReferences) throws ResourceException {
+    ScimUser resource = patchHandler.apply(get(id), patchOperations);
     users.put(id, resource);
     return resource;
   }