🌐 Section 6 · Question #4

What is the difference between PUT and PATCH

Both PUT and PATCH are HTTP methods used to update existing resources on a server, but they possess fundamentally different architectural semantics, operational scopes, and data...


🟢 Junior Level

Both PUT and PATCH are HTTP methods used to update existing resources on a server, but they possess fundamentally different architectural semantics, operational scopes, and data-handling rules:

Core Conceptual Differences

  • PUT (Complete Resource Replacement — RFC 9110):
    • The client provides a complete representation of the target resource.
    • Any existing fields that are omitted from the PUT payload must be cleared (set to null) or reset to schema default values.
    • The method is strictly idempotent ($f(f(x)) = f(x)$).
    • SQL Analogy: UPDATE users SET name = ?, age = ?, email = ? WHERE id = ?;
  • PATCH (Partial Resource Modification — RFC 5789):
    • The client transmits only the specific fields or mutation instructions that need to change.
    • All omitted fields retain their existing values in the database.
    • Minimizes network payload size when modifying large objects with dozens of attributes.
    • The method is not guaranteed to be idempotent (depends on the nature of the patch operation).
    • SQL Analogy: UPDATE users SET age = ? WHERE id = ?;

Concrete Request-Response Walkthrough

Assume the database currently holds User 42:

{
  "id": 42,
  "name": "Alex",
  "age": 25,
  "email": "alex@example.com"
}

Scenario A: Executing a PUT Request (Full Replacement)

PUT /api/v1/users/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "name": "Alex",
  "age": 26
}

Database Result: age is updated to 26, but because email was omitted from the complete replacement payload, the database resets email to null!

Scenario B: Executing a PATCH Request (Partial Update)

PATCH /api/v1/users/42 HTTP/1.1
Host: api.example.com
Content-Type: application/json

{
  "age": 26
}

Database Result: Only age is updated to 26; email remains untouched as "alex@example.com".


🟡 Middle Level

Specification Comparison Matrix: RFC 9110 vs. RFC 5789

Architectural Criterion PUT (RFC 9110) PATCH (RFC 5789)
Semantic Intent Complete replacement of target resource representation Application of partial delta mutations to target resource
Idempotency Guarantee ✅ Guaranteed Idempotent ❌ Non-idempotent by default (operation-dependent)
Safety (Safe Method) ❌ Unsafe (causes database mutations) ❌ Unsafe (causes database mutations)
Standard Media Types application/json, application/xml application/merge-patch+json, application/json-patch+json
Creation Capability Can create a resource if the URI is client-assigned Purely designed for modifying existing resources
Bandwidth Consumption High (transfers full entity representation) Low (transfers only field deltas)

The Two Official IETF Standards for PATCH

The HTTP working group established two formal specifications for implementing partial updates:

1. JSON Merge Patch (RFC 7386) — Declarative Delta

The client transmits a partial JSON object using Content-Type: application/merge-patch+json. The server merges the supplied keys directly over the existing entity representation:

{
  "email": "alex.new@example.com",
  "phoneNumber": null
}
  • In RFC 7386, an explicit "field": null instruction directs the server to delete or clear that attribute, whereas omitting a field directs the server to leave it untouched.

2. JSON Patch (RFC 6902) — Imperative Mutation Sequence

The client transmits an ordered array of atomic operation instructions using Content-Type: application/json-patch+json and JSON Pointer (RFC 6901) syntax:

[
  { "op": "test", "path": "/version", "value": 4 },
  { "op": "replace", "path": "/age", "value": 26 },
  { "op": "remove", "path": "/temporaryToken" },
  { "op": "add", "path": "/tags/-", "value": "premium" }
]
  • Supported operations: add, remove, replace, move, copy, and test.
  • If the test operation fails (e.g., entity version has changed), the entire patch transaction aborts atomically.

🔴 Senior Level

Production Implementation in Spring Boot 3

package com.example.controller;

import com.example.dto.ReplaceUserRequest;
import com.example.dto.UserResponseDto;
import com.example.service.UserService;
import com.fasterxml.jackson.databind.JsonNode;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    // 1. PUT: Full representation replacement
    @PutMapping("/{id}")
    public ResponseEntity<UserResponseDto> replaceUser(
            @PathVariable Long id, 
            @Valid @RequestBody ReplaceUserRequest request) {
        return ResponseEntity.ok(userService.replaceUser(id, request));
    }

    // 2. PATCH: Partial update via JSON Merge Patch
    @PatchMapping(path = "/{id}", consumes = "application/merge-patch+json")
    public ResponseEntity<UserResponseDto> patchUser(
            @PathVariable Long id, 
            @RequestBody JsonNode patchPayload) {
        return ResponseEntity.ok(userService.patchUser(id, patchPayload));
    }
}
package com.example.service;

import com.example.dto.ReplaceUserRequest;
import com.example.dto.UserResponseDto;
import com.example.entity.User;
import com.example.exception.ResourceNotFoundException;
import com.example.repository.UserRepository;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public UserResponseDto replaceUser(Long id, ReplaceUserRequest req) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));

        // PUT completely overwrites all mutable fields, resetting omitted attributes to null
        user.setName(req.name());
        user.setAge(req.age());
        user.setEmail(req.email()); // If req.email() is null, database value is cleared!
        
        return UserResponseDto.fromEntity(user);
    }

    @Transactional
    public UserResponseDto patchUser(Long id, JsonNode patchPayload) {
        User user = userRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("User not found: " + id));

        // Evaluate only fields explicitly supplied in the partial JSON payload
        if (patchPayload.has("name")) {
            user.setName(patchPayload.get("name").asText());
        }
        if (patchPayload.has("age")) {
            user.setAge(patchPayload.get("age").asInt());
        }
        if (patchPayload.has("email")) {
            // Distinguish explicit null from omitted field
            user.setEmail(patchPayload.get("email").isNull() ? null : patchPayload.get("email").asText());
        }

        return UserResponseDto.fromEntity(user);
    }
}

The “Three-State Null Problem” in Java DTOs

Standard Java Jackson deserialization maps both an omitted JSON property and an explicit "field": null to Java’s null. This prevents the application from discerning whether the client intended to clear the field or leave it untouched.

To solve this cleanly in type-safe DTOs, use JsonNullable<T> from org.openapitools.jackson.nullable.JsonNullable:

package com.example.dto;

import org.openapitools.jackson.nullable.JsonNullable;

public class PatchUserRequest {
    // 1. Omitted from JSON:       email.isUndefined() == true
    // 2. Explicit "email": null:  email.isPresent() == true && email.get() == null
    // 3. Explicit "email": "x@y": email.isPresent() == true && email.get().equals("x@y")
    private JsonNullable<String> email = JsonNullable.undefined();
    private JsonNullable<Integer> age = JsonNullable.undefined();

    // Getters and Setters
    public JsonNullable<String> getEmail() { return email; }
    public void setEmail(JsonNullable<String> email) { this.email = email; }

    public JsonNullable<Integer> getAge() { return age; }
    public void setAge(JsonNullable<Integer> age) { this.age = age; }
}

Why PATCH Can Be NON-IDEMPOTENT

While PUT is fundamentally idempotent, PATCH can easily break idempotency depending on its operational logic:

  1. Idempotent PATCH (Deterministic Property Replacement):
    { "op": "replace", "path": "/status", "value": "SUSPENDED" }
    

    Executing this request 1 time or 100 times results in the identical final state (status = "SUSPENDED").

  2. Non-Idempotent PATCH (Array Append):
    { "op": "add", "path": "/roles/-", "value": "AUDITOR" }
    

    Each successive request appends another "AUDITOR" entry to the end of the roles array (["USER", "AUDITOR", "AUDITOR", "AUDITOR"]), modifying server state on every invocation.

  3. Non-Idempotent PATCH (Numeric Increment):
    { "op": "increment", "path": "/wallet/balance", "value": 50 }
    

    Executing this 3 times increases the balance by 150 instead of 50.


4 Tricky Questions

1. What is the server obligated to do under RFC 9110 if a client executes a PUT request that omits an optional field?

Answer: Under RFC 9110, PUT specifies a complete replacement of the target resource representation. The server is required to replace the existing stored resource with the exact representation supplied.

  • Any fields omitted by the client must be cleared (set to null) or reset to their default schema values.
  • The server must not assume omitted fields should retain their prior state. If the client wants to update only a subset of fields while keeping the others unchanged, it is an architectural error to issue a PUT; the client must issue a PATCH.

2. Give two concrete examples where a PATCH request is non-idempotent.

Answer:

  1. JSON Patch Array Append: [{"op": "add", "path": "/tags/-", "value": "featured"}] The - index denotes appending to the end of the array. Calling this endpoint 3 times appends 3 identical items to the list.
  2. Relative Numeric Increments: An operation modifying a counter or account balance via delta offsets ({"op": "increment", "path": "/retryCount", "value": 1}). Each repeated execution increments the counter further, resulting in different server states.

3. How do RFC 7386 (JSON Merge Patch) and RFC 6902 (JSON Patch) differ architecturally?

Answer:

  • RFC 7386 (JSON Merge Patch):
    • Paradigm: Declarative state merge.
    • Format: Standard JSON document containing target fields.
    • Null Semantics: An explicit null deletes the attribute; omitted attributes are preserved.
    • Limitation: Cannot partially modify arrays or manipulate individual list indices; arrays are replaced in their entirety.
  • RFC 6902 (JSON Patch):
    • Paradigm: Imperative sequence of transactional instructions.
    • Format: An array of operation objects (op, path, value).
    • Features: Supports fine-grained list manipulation (add, remove, move, copy), conditional verification (test), and atomic transaction aborts if any step fails.

4. How do you resolve the “Three-State Null Problem” in Java Spring Boot applications?

Answer: The problem is solved using JsonNullable<T> (from org.openapitools.jackson.nullable:jackson-databind-nullable):

  • JsonNullable.undefined() represents an attribute that was omitted from the incoming JSON payload (the service skips updating this column).
  • JsonNullable.of(null) represents an attribute explicitly declared as "field": null in the payload (the service sets the database column to null).
  • JsonNullable.of(value) represents a concrete updated value (the service updates the database column). Alternatively, developers can parse raw requests via Jackson’s JsonNode and check patchNode.has("field") vs patchNode.get("field").isNull().

🎯 Interview Cheat Sheet

30-Second Elevator Pitch

  • PUT (RFC 9110) is for complete resource replacement. The client supplies the entire representation; missing fields are reset to null or defaults. It is strictly idempotent.
  • PATCH (RFC 5789) is for partial resource updates. The client sends only modified fields or instructions, conserving network bandwidth. It is not guaranteed to be idempotent (e.g., array appends or increments).
  • Production standard implementations rely on RFC 7386 (JSON Merge Patch) for simple field merging or RFC 6902 (JSON Patch) for transactional instruction sequences. In Java, handle omitted vs. null values using JsonNullable<T>.

Red Flags (What NOT to Say)

  • ❌ “PUT and PATCH are identical synonyms; it is just a matter of team preference.” (A severe violation of HTTP specifications; PUT replaces, PATCH modifies).
  • ❌ “PUT only updates the fields sent in the request and keeps the rest intact.” (This is the definition of PATCH; PUT resets omitted fields).
  • ❌ “PATCH is always idempotent just like PUT.” (RFC 5789 explicitly notes that PATCH is non-idempotent by default because operations like list appends alter state on repetition).
  • ❌ “There is no formal standard for PATCH in HTTP.” (RFC 5789 defines the method, RFC 7386 defines Merge Patch, and RFC 6902 defines JSON Patch).