diff --git a/spring-session-core/src/main/java/org/springframework/session/MapReactorSessionRepository.java b/spring-session-core/src/main/java/org/springframework/session/MapReactorSessionRepository.java
new file mode 100644
index 00000000..a2b4bc0b
--- /dev/null
+++ b/spring-session-core/src/main/java/org/springframework/session/MapReactorSessionRepository.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2014-2017 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 org.springframework.session;
+
+import org.springframework.session.events.SessionDeletedEvent;
+import org.springframework.session.events.SessionExpiredEvent;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A {@link SessionRepository} backed by a {@link Map} and that uses a
+ * {@link MapSession}. By default a {@link ConcurrentHashMap} is
+ * used, but a custom {@link Map} can be injected to use distributed maps
+ * provided by NoSQL stores like Redis and Hazelcast.
+ *
+ *
+ * The implementation does NOT support firing {@link SessionDeletedEvent} or
+ * {@link SessionExpiredEvent}.
+ *
+ *
+ * @author Rob Winch
+ * @since 2.0
+ */
+public class MapReactorSessionRepository implements ReactorSessionRepository {
+ /**
+ * If non-null, this value is used to override
+ * {@link Session#setMaxInactiveInterval(Duration)}.
+ */
+ private Integer defaultMaxInactiveInterval;
+
+ private final Map sessions;
+
+ /**
+ * Creates an instance backed by a {@link ConcurrentHashMap}.
+ */
+ public MapReactorSessionRepository() {
+ this(new ConcurrentHashMap<>());
+ }
+
+ /**
+ * Creates a new instance backed by the provided {@link Map}. This allows
+ * injecting a distributed {@link Map}.
+ *
+ * @param sessions the {@link Map} to use. Cannot be null.
+ */
+ public MapReactorSessionRepository(Map sessions) {
+ if (sessions == null) {
+ throw new IllegalArgumentException("sessions cannot be null");
+ }
+ this.sessions = sessions;
+ }
+
+ /**
+ * Creates a new instance backed by the provided {@link Map}. This allows
+ * injecting a distributed {@link Map}.
+ *
+ * @param sessions the {@link Map} to use. Cannot be null.
+ */
+ public MapReactorSessionRepository(Session... sessions) {
+ if (sessions == null) {
+ throw new IllegalArgumentException("sessions cannot be null");
+ }
+ this.sessions = new ConcurrentHashMap<>();
+ for(Session session : sessions) {
+ this.performSave(session);
+ }
+ }
+
+ /**
+ * Creates a new instance backed by the provided {@link Map}. This allows
+ * injecting a distributed {@link Map}.
+ *
+ * @param sessions the {@link Map} to use. Cannot be null.
+ */
+ public MapReactorSessionRepository(Iterable sessions) {
+ if (sessions == null) {
+ throw new IllegalArgumentException("sessions cannot be null");
+ }
+ this.sessions = new ConcurrentHashMap<>();
+ for(Session session : sessions) {
+ this.performSave(session);
+ }
+ }
+
+ /**
+ * If non-null, this value is used to override
+ * {@link Session#setMaxInactiveInterval(Duration)}.
+ * @param defaultMaxInactiveInterval the number of seconds that the {@link Session}
+ * should be kept alive between client requests.
+ */
+ public void setDefaultMaxInactiveInterval(int defaultMaxInactiveInterval) {
+ this.defaultMaxInactiveInterval = Integer.valueOf(defaultMaxInactiveInterval);
+ }
+
+ public Mono save(Session session) {
+ return Mono.fromRunnable(() -> performSave(session));
+ }
+
+ private void performSave(Session session) {
+ this.sessions.put(session.getId(), new MapSession(session));
+ }
+
+ public Mono findById(String id) {
+ return Mono.defer(() -> {
+ Session saved = this.sessions.get(id);
+ if (saved == null) {
+ return Mono.empty();
+ }
+ if (saved.isExpired()) {
+ delete(saved.getId());
+ return Mono.empty();
+ }
+ return Mono.just(new MapSession(saved));
+ });
+ }
+
+ public Mono delete(String id) {
+ return Mono.fromRunnable(() -> this.sessions.remove(id));
+ }
+
+ public Mono createSession() {
+ return Mono.defer(() -> {
+ Session result = new MapSession();
+ if (this.defaultMaxInactiveInterval != null) {
+ result.setMaxInactiveInterval(
+ Duration.ofSeconds(this.defaultMaxInactiveInterval));
+ }
+ return Mono.just(result);
+ });
+ }
+}
diff --git a/spring-session-core/src/test/java/org/springframework/session/MapReactorSessionRepositoryTests.java b/spring-session-core/src/test/java/org/springframework/session/MapReactorSessionRepositoryTests.java
new file mode 100644
index 00000000..3da5b976
--- /dev/null
+++ b/spring-session-core/src/test/java/org/springframework/session/MapReactorSessionRepositoryTests.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2014-2017 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 org.springframework.session;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Rob Winch
+ * @since 2.0
+ */
+public class MapReactorSessionRepositoryTests {
+ MapReactorSessionRepository repository;
+
+ MapSession session;
+
+ @Before
+ public void setup() {
+ this.repository = new MapReactorSessionRepository();
+ this.session = new MapSession("session-id");
+ }
+
+ @Test
+ public void constructorVarargsThenFound() {
+ this.repository = new MapReactorSessionRepository(this.session);
+
+ Session findByIdSession = this.repository.findById(this.session.getId()).block();
+ assertThat(findByIdSession).isNotNull();
+ assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorVarargsWhenNullThenThrowsIllegalArgumentException() {
+ Session[] sessions = null;
+ new MapReactorSessionRepository(sessions);
+ }
+
+ @Test
+ public void constructorIterableThenFound() {
+ this.repository = new MapReactorSessionRepository(Arrays.asList(this.session));
+
+ Session findByIdSession = this.repository.findById(this.session.getId()).block();
+ assertThat(findByIdSession).isNotNull();
+ assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorIterableWhenNullThenThrowsIllegalArgumentException() {
+ Iterable sessions = null;
+ new MapReactorSessionRepository(sessions);
+ }
+
+ @Test
+ public void constructorMapThenFound() {
+ Map sessions = new HashMap<>();
+ sessions.put(this.session.getId(), this.session);
+ this.repository = new MapReactorSessionRepository(sessions);
+
+ Session findByIdSession = this.repository.findById(this.session.getId()).block();
+ assertThat(findByIdSession).isNotNull();
+ assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void constructorMapWhenNullThenThrowsIllegalArgumentException() {
+ Map sessions = null;
+ new MapReactorSessionRepository(sessions);
+ }
+
+ @Test
+ public void saveWhenNoSubscribersThenNotFound() {
+ this.repository.save(this.session);
+
+ assertThat(this.repository.findById(this.session.getId()).block()).isNull();
+ }
+
+ @Test
+ public void saveWhenSubscriberThenFound() {
+ this.repository.save(this.session).block();
+
+ Session findByIdSession = this.repository.findById(this.session.getId()).block();
+ assertThat(findByIdSession).isNotNull();
+ assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
+ }
+
+ @Test
+ public void findByIdWhenNotExpiredThenFound() {
+ this.repository = new MapReactorSessionRepository(this.session);
+
+ Session findByIdSession = this.repository.findById(this.session.getId()).block();
+ assertThat(findByIdSession).isNotNull();
+ assertThat(findByIdSession.getId()).isEqualTo(this.session.getId());
+ }
+
+ @Test
+ public void findByIdWhenExpiredThenEmpty() {
+ this.session.setMaxInactiveInterval(Duration.ofSeconds(1));
+ this.session.setLastAccessedTime(Instant.now().minus(5, ChronoUnit.MINUTES));
+ this.repository = new MapReactorSessionRepository(Arrays.asList(this.session));
+
+ assertThat(this.repository.findById(this.session.getId()).block()).isNull();
+ }
+
+ @Test
+ public void createSessionWhenDefaultMaxInactiveIntervalThenDefaultMaxInactiveInterval() {
+ Session session = this.repository.createSession().block();
+
+ assertThat(session).isInstanceOf(MapSession.class);
+ assertThat(session.getMaxInactiveInterval())
+ .isEqualTo(new MapSession().getMaxInactiveInterval());
+ }
+
+ @Test
+ public void createSessionWhenCustomMaxInactiveIntervalThenCustomMaxInactiveInterval() {
+ final Duration expectedMaxInterval = new MapSession().getMaxInactiveInterval()
+ .plusSeconds(10);
+ this.repository.setDefaultMaxInactiveInterval(
+ (int) expectedMaxInterval.getSeconds());
+
+ Session session = this.repository.createSession().block();
+
+ assertThat(session.getMaxInactiveInterval())
+ .isEqualTo(expectedMaxInterval);
+ }
+}
\ No newline at end of file