+ * If you store the object in a SQL database, the returned object might have
+ * a new id or updated consistency version.
+ *
+ * @param value
+ * the object to save
+ * @return the fresh object or {@code null} if no object was found to update
+ */
+ @Nullable
+ T save(T value);
+
+ /**
+ * Deletes the object with the given id.
+ *
+ * @param id
+ * the id of the object to delete
+ */
+ void delete(ID id);
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java
new file mode 100644
index 00000000000..4f7265d8892
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/GetService.java
@@ -0,0 +1,30 @@
+package com.vaadin.flow.spring.data;
+
+import java.util.Optional;
+
+/**
+ * A browser-callable service that can fetch the given type of object.
+ */
+public interface GetService
+ * Custom filter implementations need to handle this filter by running all child
+ * filters and verifying that all of them pass.
+ */
+public class AndFilter extends Filter {
+ private List
+ * Note: The passed in {@code filterTransformation} function is applied
+ * after all other mappings are applied.
+ *
+ * @param filterTransformation
+ * The function to apply.
+ * @return This instance.
+ */
+ public FilterTransformer withFilterTransformation(
+ Function
+ * Note: The passed in {@code filterTransformation} function is not applied
+ * on pageables.
+ *
+ * @param pageable
+ * The pageable instance to transform.
+ * @return The transformed pageable.
+ */
+ public Pageable apply(Pageable pageable) {
+ if (pageable == null) {
+ return null;
+ }
+
+ var orders = pageable.getSort().stream().map(order -> {
+ var mappedProperty = mappings.get(order.getProperty());
+ return mappedProperty == null ? order
+ : new Sort.Order(order.getDirection(), mappedProperty);
+ }).toList();
+
+ return PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(),
+ Sort.by(orders));
+ }
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java
new file mode 100644
index 00000000000..83544f81919
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java
@@ -0,0 +1,28 @@
+package com.vaadin.flow.spring.data.filter;
+
+import java.util.List;
+
+/**
+ * A filter that requires at least one of its children to pass.
+ *
+ * Custom filter implementations need to handle this filter by running all child
+ * filters and verifying that at least one of them passes.
+ */
+public class OrFilter extends Filter {
+
+ private List
+ * Custom filter implementations need to handle this filter by:
+ *
+ *
+ */
+@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY)
+@JsonSubTypes({ @Type(value = OrFilter.class, name = "or"),
+ @Type(value = AndFilter.class, name = "and"),
+ @Type(value = PropertyStringFilter.class, name = "propertyString") })
+public class Filter {
+
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java
new file mode 100644
index 00000000000..56581410a38
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/FilterTransformer.java
@@ -0,0 +1,125 @@
+package com.vaadin.flow.spring.data.filter;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.domain.Sort;
+
+/**
+ * Utility class for transforming property names in filters and pageable
+ * objects.
+ */
+public class FilterTransformer {
+
+ private final Map
+ *
+ */
+public class PropertyStringFilter extends Filter {
+ public enum Matcher {
+ EQUALS, CONTAINS, LESS_THAN, GREATER_THAN;
+ }
+
+ private String propertyId;
+ private String filterValue;
+ private Matcher matcher;
+
+ /**
+ * Gets the property, or nested property path, to filter by. For example
+ * {@code "name"} or {@code "address.city"}.
+ *
+ * @return the property name
+ */
+ public String getPropertyId() {
+ return propertyId;
+ }
+
+ /**
+ * Sets the property, or nested property path, to filter by.
+ *
+ * @param propertyId
+ * the property name
+ */
+ public void setPropertyId(String propertyId) {
+ this.propertyId = propertyId;
+ }
+
+ /**
+ * Gets the filter value to compare against. The filter value is always
+ * stored as a string, but can represent multiple types of values using
+ * specific formats. For example, when filtering a property of type
+ * {@code LocalDate}, the filter value could be {@code "2020-01-01"}. The
+ * actual filter implementation is responsible for parsing the filter value
+ * into the correct type to use for querying the underlying data layer.
+ *
+ * @return the filter value
+ */
+ public String getFilterValue() {
+ return filterValue;
+ }
+
+ /**
+ * Sets the filter value to compare against.
+ *
+ * @param filterValue
+ * the filter value
+ */
+ public void setFilterValue(String filterValue) {
+ this.filterValue = filterValue;
+ }
+
+ /**
+ * The matcher, or operator, to use when comparing the property value to the
+ * filter value.
+ *
+ * @return the matcher
+ */
+ public Matcher getMatcher() {
+ return matcher;
+ }
+
+ /**
+ * Sets the matcher, or operator, to use when comparing the property value
+ * to the filter value.
+ *
+ * @param type
+ * the matcher
+ */
+ public void setMatcher(Matcher type) {
+ this.matcher = type;
+ }
+
+ @Override
+ public String toString() {
+ return "PropertyStringFilter [propertyId=" + propertyId + ", matcher="
+ + matcher + ", filterValue=" + filterValue + "]";
+ }
+
+}
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java
new file mode 100644
index 00000000000..e93d497fe45
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/package-info.java
@@ -0,0 +1,2 @@
+@org.springframework.lang.NonNullApi
+package com.vaadin.flow.spring.data.filter;
diff --git a/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java
new file mode 100644
index 00000000000..e3aa13fcc5e
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/package-info.java
@@ -0,0 +1,2 @@
+@org.springframework.lang.NonNullApi
+package com.vaadin.flow.spring.data;