diff --git a/r2dbc/pom.xml b/r2dbc/pom.xml
index 5c6e2732..fbb69575 100644
--- a/r2dbc/pom.xml
+++ b/r2dbc/pom.xml
@@ -18,6 +18,7 @@
example
+ query-by-example
diff --git a/r2dbc/query-by-example/README.adoc b/r2dbc/query-by-example/README.adoc
new file mode 100644
index 00000000..37015d3a
--- /dev/null
+++ b/r2dbc/query-by-example/README.adoc
@@ -0,0 +1,35 @@
+== Spring Data R2DBC - Query-by-Example (QBE) example
+
+This project contains samples of Query-by-Example of Spring Data R2DBC.
+
+=== Support for Query-by-Example
+
+Query by Example (QBE) is a user-friendly querying technique with a simple interface.
+It allows dynamic query creation and does not require to write queries containing field names.
+In fact, Query by Example does not require to write queries using SQL at all.
+
+An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties.
+You can use Query by Example with Repositories.
+
+[source,java]
+----
+interface PersonRepository extends ReactiveCrudRepository, ReactiveQueryByExampleExecutor {
+}
+----
+
+[source,java]
+----
+Example example = Example.of(new Person("Jon", "Snow"));
+repo.findAll(example);
+
+
+ExampleMatcher matcher = ExampleMatcher.matching().
+ .withMatcher("firstname", endsWith())
+ .withMatcher("lastname", startsWith().ignoreCase());
+
+Example example = Example.of(new Person("Jon", "Snow"), matcher);
+repo.count(example);
+----
+
+This example contains shows the usage with `PersonRepositoryIntegrationTests`.
+
diff --git a/r2dbc/query-by-example/pom.xml b/r2dbc/query-by-example/pom.xml
new file mode 100644
index 00000000..ec3fc86c
--- /dev/null
+++ b/r2dbc/query-by-example/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+
+ org.springframework.data.examples
+ spring-data-r2dbc-examples
+ 2.0.0.BUILD-SNAPSHOT
+
+
+ spring-data-r2dbc-query-by-example
+
+ Spring Data R2DBC - Query by Example
+
+
diff --git a/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java
new file mode 100644
index 00000000..775afbd5
--- /dev/null
+++ b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/Person.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 example.springdata.r2dbc.basics;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+import org.springframework.data.annotation.Id;
+
+/**
+ * Sample person class.
+ *
+ * @author Mark Paluch
+ */
+@Data
+@AllArgsConstructor
+class Person {
+
+ @Id Integer id;
+ String firstname, lastname;
+ Integer age;
+
+ boolean hasId() {
+ return id != null;
+ }
+}
diff --git a/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java
new file mode 100644
index 00000000..fe267454
--- /dev/null
+++ b/r2dbc/query-by-example/src/main/java/example/springdata/r2dbc/basics/PersonRepository.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 example.springdata.r2dbc.basics;
+
+import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+
+/**
+ * Simple repository interface for {@link Person} instances. The interface implements
+ * {@link ReactiveQueryByExampleExecutor} and allows execution of methods accepting
+ * {@link org.springframework.data.domain.Example}.
+ *
+ * @author Mark Paluch
+ */
+interface PersonRepository extends ReactiveCrudRepository, ReactiveQueryByExampleExecutor {}
diff --git a/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java
new file mode 100644
index 00000000..f1ecf3a5
--- /dev/null
+++ b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/InfrastructureConfiguration.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed 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
+ *
+ * https://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 example.springdata.r2dbc.basics;
+
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+/**
+ * @author Mark Paluch
+ */
+@SpringBootApplication
+@EnableTransactionManagement
+class InfrastructureConfiguration {}
diff --git a/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java
new file mode 100644
index 00000000..63007c2d
--- /dev/null
+++ b/r2dbc/query-by-example/src/test/java/example/springdata/r2dbc/basics/PersonRepositoryIntegrationTests.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2021 the original author or authors.
+ *
+ * Licensed 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 example.springdata.r2dbc.basics;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.springframework.data.domain.ExampleMatcher.*;
+import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;
+
+import reactor.core.publisher.Hooks;
+import reactor.test.StepVerifier;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Example;
+import org.springframework.r2dbc.core.DatabaseClient;
+
+/**
+ * Integration test showing the usage of R2DBC Query-by-Example support through Spring Data repositories.
+ *
+ * @author Mark Paluch
+ */
+@SpringBootTest(classes = InfrastructureConfiguration.class)
+class PersonRepositoryIntegrationTests {
+
+ @Autowired PersonRepository people;
+ @Autowired DatabaseClient client;
+
+ private Person skyler, walter, flynn, marie, hank;
+
+ @BeforeEach
+ void setUp() {
+
+ Hooks.onOperatorDebug();
+
+ List statements = Arrays.asList(//
+ "DROP TABLE IF EXISTS person;",
+ "CREATE TABLE person (id SERIAL PRIMARY KEY, firstname VARCHAR(100) NOT NULL, lastname VARCHAR(100) NOT NULL, age INTEGER NOT NULL);");
+
+ this.skyler = new Person(null, "Skyler", "White", 45);
+ this.walter = new Person(null, "Walter", "White", 50);
+ this.flynn = new Person(null, "Walter Jr. (Flynn)", "White", 17);
+ this.marie = new Person(null, "Marie", "Schrader", 38);
+ this.hank = new Person(null, "Hank", "Schrader", 43);
+
+ statements.stream().map(client::sql) //
+ .map(DatabaseClient.GenericExecuteSpec::then) //
+ .forEach(it -> it.as(StepVerifier::create).verifyComplete());
+
+ people.saveAll(Arrays.asList(skyler, walter, flynn, marie, hank)) //
+ .as(StepVerifier::create) //
+ .expectNextCount(5) //
+ .verifyComplete();
+ }
+
+ @Test
+ void countBySimpleExample() {
+
+ Example example = Example.of(new Person(null, null, "White", null));
+
+ people.count(example).as(StepVerifier::create) //
+ .expectNext(3L) //
+ .verifyComplete();
+ }
+
+ @Test
+ void ignorePropertiesAndMatchByAge() {
+
+ Example example = Example.of(flynn, matching(). //
+ withIgnorePaths("firstname", "lastname"));
+
+ people.findOne(example) //
+ .as(StepVerifier::create) //
+ .expectNext(flynn) //
+ .verifyComplete();
+ }
+
+ @Test
+ void substringMatching() {
+
+ Example example = Example.of(new Person(null, "er", null, null), matching(). //
+ withStringMatcher(StringMatcher.ENDING));
+
+ people.findAll(example).collectList() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).containsExactlyInAnyOrder(skyler, walter);
+ }) //
+ .verifyComplete();
+ }
+
+ @Test
+ void matchStartingStringsIgnoreCase() {
+
+ Example example = Example.of(new Person(null, "Walter", "WHITE", null), matching(). //
+ withIgnorePaths("age"). //
+ withMatcher("firstname", startsWith()). //
+ withMatcher("lastname", ignoreCase()));
+
+ people.findAll(example).collectList() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).containsExactlyInAnyOrder(flynn, walter);
+ }) //
+ .verifyComplete();
+ }
+
+ @Test
+ void configuringMatchersUsingLambdas() {
+
+ Example example = Example.of(new Person(null, "Walter", "WHITE", null), matching(). //
+ withIgnorePaths("age"). //
+ withMatcher("firstname", GenericPropertyMatcher::startsWith). //
+ withMatcher("lastname", GenericPropertyMatcher::ignoreCase));
+
+ people.findAll(example).collectList() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).containsExactlyInAnyOrder(flynn, walter);
+ }) //
+ .verifyComplete();
+ }
+
+ @Test
+ void valueTransformer() {
+
+ Example example = Example.of(new Person(null, null, "White", 99), matching(). //
+ withMatcher("age", matcher -> matcher.transform(value -> Optional.of(50))));
+
+ people.findAll(example).collectList() //
+ .as(StepVerifier::create) //
+ .consumeNextWith(actual -> {
+ assertThat(actual).containsExactlyInAnyOrder(walter);
+ }) //
+ .verifyComplete();
+ }
+}