+ * 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; will never be {@literal null}.
+ */
+ 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..1f7cf3eaca1
--- /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 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
+ * 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:
+ *
+ * The returned objects might have new ids or updated consistency versions.
+ *
+ * @param values
+ * the objects to save
+ * @return the fresh objects
+ */
+ public List
+ * If the filter contains {@link PropertyStringFilter} instances, their
+ * properties, or nested property paths, need to match the structure of the
+ * entity class. Likewise, their filter values should be in a format that
+ * can be parsed into the type that the property is of.
+ *
+ * @param
+ *
+ */
+@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 implements Serializable {
+
+}
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..4529658ff45
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/filter/OrFilter.java
@@ -0,0 +1,45 @@
+package com.vaadin.flow.spring.data.filter;
+
+import java.util.List;
+
+/**
+ * A filter that requires at least one of its children to pass.
+ *
+ *
+ */
+public class PropertyStringFilter extends Filter {
+ public enum Matcher {
+ EQUALS, CONTAINS, LESS_THAN, GREATER_THAN;
+ }
+
+ private String propertyId;
+ private String filterValue;
+ private Matcher matcher;
+
+ /**
+ * Create an empty filter.
+ */
+ public PropertyStringFilter() {
+ // Empty constructor is needed for serialization
+ }
+
+ /**
+ * Create a filter with the given property, matcher and filter value.
+ *
+ * @param propertyId
+ * the property id, or a nested property path where each property
+ * is separated by a dot
+ * @param matcher
+ * the matcher to use when comparing the property value to the
+ * filter value
+ * @param filterValue
+ * the filter value to compare against
+ */
+ public PropertyStringFilter(String propertyId, Matcher matcher,
+ String filterValue) {
+ this.propertyId = propertyId;
+ this.matcher = matcher;
+ this.filterValue = filterValue;
+ }
+
+ /**
+ * 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 getClass().getSimpleName() + " [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/jpa/CrudRepositoryService.java b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java
new file mode 100644
index 00000000000..c5ff6e269e6
--- /dev/null
+++ b/vaadin-spring/src/main/java/com/vaadin/flow/spring/data/jpa/CrudRepositoryService.java
@@ -0,0 +1,71 @@
+package com.vaadin.flow.spring.data.jpa;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jspecify.annotations.Nullable;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.repository.CrudRepository;
+
+import com.vaadin.flow.spring.data.CrudService;
+
+/**
+ * A service that delegates crud operations to a JPA repository.
+ *
+ * @param R findBy(
+ Specification S save(S entity) {
+ return null;
+ }
+
+ @Override
+ public Iterable saveAll(
+ Iterable entities) {
+ return null;
+ }
+
+ @Override
+ public Optional S saveAndFlush(S entity) {
+ return null;
+ }
+
+ @Override
+ public List saveAllAndFlush(
+ Iterable entities) {
+ return null;
+ }
+
+ @Override
+ public void deleteAllInBatch(Iterable List findAll(Example example) {
+ return null;
+ }
+
+ @Override
+ public List findAll(Example example,
+ Sort sort) {
+ return null;
+ }
+
+ @Override
+ public Optional R findBy(
+ Specification List saveAll(Iterable entities) {
+ return null;
+ }
+
+ @Override
+ public List S save(S entity) {
+ return null;
+ }
+
+ @Override
+ public Optional Optional findOne(Example example) {
+ return Optional.empty();
+ }
+
+ @Override
+ public Page findAll(Example example,
+ Pageable pageable) {
+ return null;
+ }
+
+ @Override
+ public long count(Example example) {
+ return 0;
+ }
+
+ @Override
+ public boolean exists(Example example) {
+ return false;
+ }
+
+ @Override
+ public R findBy(Example example,
+ Function