Summary
Add a first-class, explicitly named patch API on Mongo repositories / MongoOperations that applies a typed partial entity as a $set-only update against the repository’s aggregate type T, without loading the full document and without replacing unmapped fields.
Naming parallel with the existing API:
save(entity) — persist / replace the aggregate
patch(partialEntity) — write only the fields mapped by the partial type
This is not a request to change save() semantics and not a request to treat a half-populated instance of T as an update stamp. A partial entity is a dedicated type whose mapped properties are a subset of T.
Motivation
In systems where multiple services share a MongoDB collection (or where a service only owns a subset of fields), the current options are awkward:
CrudRepository.save(entity) replaces the whole document. When the Java type maps only a subset of fields, properties that exist in MongoDB but are absent from that type are not preserved across the write.
MongoTemplate + Update works, but pushes teams toward untyped field paths / JSON update strings.
- Repository
@Update is useful for fixed updates, but does not scale well for “apply this typed slice of fields” use cases.
Teams often invent local helpers (converter.write → $set). A supported patch would make the intent explicit and keep that pattern consistent.
Concrete scenario
- Collection
customers
- Billing service maps
{ id, billing.plan, billing.status }
- Shipping service maps
{ id, shipping.address }
- Billing needs to update its fields while leaving
shipping.* unchanged
With the current API, persisting a billing-only mapped type through save(...) performs a full document replace, so fields outside that type are not retained. A dedicated patch(billingPartial) would express “write only these mapped fields” without requiring a full-document read/merge.
Why this is different from #2642
#2642 asked for partial updates from a partially filled domain POJO, and was declined mainly because:
- A half-filled aggregate as an update stamp conflicts with Spring Data’s DDD-oriented model.
- “Non-null fields are written” cannot express “set this field to
null”.
This proposal avoids both issues:
| Concern in #2642 |
This proposal |
Half-filled aggregate root (T with some nulls) |
Separate partial entity type, validated against T |
Overloaded / implicit save |
Explicit patch API; save unchanged |
| Null ambiguity |
Explicit null policy (see below) |
Proposed API
The core idea is an explicit patch operation. Where it lives can be decided by the team; a few options:
Option A — extend the existing repository type
Expose patch on MongoRepository (or a shared Spring Data abstraction, if that fits better):
<P> long patch(P partialEntity);
<P> long patch(ID id, P partialEntity);
Existing repositories would gain the method without introducing a new interface.
Option B — dedicated repository type / fragment
Keep MongoRepository unchanged and offer an opt-in type for apps that need patching:
@NoRepositoryBean
public interface PatchMongoRepository<T, ID> extends MongoRepository<T, ID> {
<P> long patch(P partialEntity);
<P> long patch(ID id, P partialEntity);
}
Or the same methods via a repository fragment, so only selected repositories opt in.
Option C — MongoOperations first
Start at the template level, then decide whether/how to surface it on repositories:
<T, P> UpdateResult patch(P partialEntity, Class<T> entityClass);
<T, P> UpdateResult patch(Object id, P partialEntity, Class<T> entityClass);
<T, P> UpdateResult patch(Query query, P partialEntity, Class<T> entityClass);
Example (shape of the call)
@Document("customers")
class Customer {
@Id String id;
Billing billing;
Shipping shipping;
}
record BillingPlanPartial(
@Id String id,
@Field("billing.plan") String plan,
@Field("billing.status") String status
) {}
// Depending on the chosen option:
// - CustomerRepository extends MongoRepository<Customer, String>
// - or CustomerRepository extends PatchMongoRepository<Customer, String>
customerRepository.patch(new BillingPlanPartial(id, "pro", "ACTIVE"));
// => db.customers.updateOne({_id: id}, {$set: {"billing.plan": "pro", "billing.status": "ACTIVE"}})
Unmapped fields such as shipping remain untouched. No full-document read is required.
Mapping rules (partial entity → $set)
- Repository / operation remains typed to aggregate
T (collection + mapping metadata come from T).
P is a partial entity type, not necessarily a subtype of T.
- Each writable property in
P maps to a Mongo field path via normal Spring Data mapping (@Field, naming strategy, etc.).
- Suggested compatibility rule: property names/types in
P must be assignable to a property path on T (runtime validation using persistent entity metadata; optional compile-time checks can live outside core).
- Nested documents should prefer dotted paths (
billing.plan) so patching does not replace an entire subdocument unless the partial entity intentionally maps the whole subdocument.
- Metadata fields such as
_id / _class are not $set unless explicitly designed that way (_id identifies the document).
Null / presence semantics
Do not use “null means skip” as the only model.
Proposed options (API detail open for discussion):
- Default for plain nullable fields: include
$set only for properties considered “present”.
- Explicit absent vs null: support wrapper types already common in the ecosystem, e.g.:
Optional<T> / dedicated patch wrapper
- or an annotation such as
@PatchNull / unset marker
- Explicit unset: allow mapping a present sentinel / annotation to
$unset.
The important part: null handling must be documented and opt-in/clear, not inferred only from Java null on a partially constructed aggregate.
Non-goals
- Changing
CrudRepository.save / replace semantics
- Treating a partially populated instance of
T as a patch stamp
- Replacing
@Update string/pipeline updates for fixed derived queries
- Requiring a Lombok-like annotation processor in spring-data-mongodb core
(compile-time partial-entity validation can be a separate community module later)
Benefits
- Strongly typed patches without untyped
Update path strings everywhere
- Fits shared collections / field ownership across services
- Clear write-side counterpart to read projections: projections for read, partial entities for
patch
- Preserves DDD purity of
save(entity) while offering an explicit escape hatch for PATCH-like persistence
Suggested acceptance criteria
Summary
Add a first-class, explicitly named
patchAPI on Mongo repositories /MongoOperationsthat applies a typed partial entity as a$set-only update against the repository’s aggregate typeT, without loading the full document and without replacing unmapped fields.Naming parallel with the existing API:
save(entity)— persist / replace the aggregatepatch(partialEntity)— write only the fields mapped by the partial typeThis is not a request to change
save()semantics and not a request to treat a half-populated instance ofTas an update stamp. A partial entity is a dedicated type whose mapped properties are a subset ofT.Motivation
In systems where multiple services share a MongoDB collection (or where a service only owns a subset of fields), the current options are awkward:
CrudRepository.save(entity)replaces the whole document. When the Java type maps only a subset of fields, properties that exist in MongoDB but are absent from that type are not preserved across the write.MongoTemplate+Updateworks, but pushes teams toward untyped field paths / JSON update strings.@Updateis useful for fixed updates, but does not scale well for “apply this typed slice of fields” use cases.Teams often invent local helpers (
converter.write→$set). A supportedpatchwould make the intent explicit and keep that pattern consistent.Concrete scenario
customers{ id, billing.plan, billing.status }{ id, shipping.address }shipping.*unchangedWith the current API, persisting a billing-only mapped type through
save(...)performs a full document replace, so fields outside that type are not retained. A dedicatedpatch(billingPartial)would express “write only these mapped fields” without requiring a full-document read/merge.Why this is different from #2642
#2642 asked for partial updates from a partially filled domain POJO, and was declined mainly because:
null”.This proposal avoids both issues:
Twith some nulls)TsavepatchAPI;saveunchangedProposed API
The core idea is an explicit
patchoperation. Where it lives can be decided by the team; a few options:Option A — extend the existing repository type
Expose
patchonMongoRepository(or a shared Spring Data abstraction, if that fits better):Existing repositories would gain the method without introducing a new interface.
Option B — dedicated repository type / fragment
Keep
MongoRepositoryunchanged and offer an opt-in type for apps that need patching:Or the same methods via a repository fragment, so only selected repositories opt in.
Option C —
MongoOperationsfirstStart at the template level, then decide whether/how to surface it on repositories:
Example (shape of the call)
Unmapped fields such as
shippingremain untouched. No full-document read is required.Mapping rules (partial entity →
$set)T(collection + mapping metadata come fromT).Pis a partial entity type, not necessarily a subtype ofT.Pmaps to a Mongo field path via normal Spring Data mapping (@Field, naming strategy, etc.).Pmust be assignable to a property path onT(runtime validation using persistent entity metadata; optional compile-time checks can live outside core).billing.plan) so patching does not replace an entire subdocument unless the partial entity intentionally maps the whole subdocument._id/_classare not$setunless explicitly designed that way (_ididentifies the document).Null / presence semantics
Do not use “null means skip” as the only model.
Proposed options (API detail open for discussion):
$setonly for properties considered “present”.Optional<T>/ dedicated patch wrapper@PatchNull/ unset marker$unset.The important part: null handling must be documented and opt-in/clear, not inferred only from Java
nullon a partially constructed aggregate.Non-goals
CrudRepository.save/ replace semanticsTas a patch stamp@Updatestring/pipeline updates for fixed derived queries(compile-time partial-entity validation can be a separate community module later)
Benefits
Updatepath strings everywherepatchsave(entity)while offering an explicit escape hatch for PATCH-like persistenceSuggested acceptance criteria
patch(partialEntity)/patch(id, partialEntity)emit$setonly for mapped partial-entity properties@Fieldand persistent property metadata ofTsavebehavior remains unchangedMongoOperations/ReactiveMongoOperations, repository support as chosen)