diff --git a/src/main/java/org/springframework/data/elasticsearch/Document.java b/src/main/java/org/springframework/data/elasticsearch/Document.java
new file mode 100644
index 000000000..703e40c25
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/Document.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2019 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 org.springframework.data.elasticsearch;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BooleanSupplier;
+import java.util.function.Function;
+import java.util.function.IntSupplier;
+import java.util.function.LongSupplier;
+import java.util.function.Supplier;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+/**
+ * A representation of a Elasticsearch document as extended {@link Map} with {@link String} keys. All iterators preserve
+ * original insertion order.
+ *
+ * Document does not allow {@code null} keys. It allows {@literal null} values.
+ *
+ * Implementing classes can bei either mutable or immutable. In case a subclass is immutable, its methods may throw
+ * {@link UnsupportedOperationException} when calling modifying methods.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+public interface Document extends Map {
+
+ /**
+ * Create a new mutable {@link Document}.
+ *
+ * @return a new {@link Document}.
+ */
+ static Document create() {
+ return new MapDocument();
+ }
+
+ /**
+ * Create a {@link Document} from a {@link Map} containing key-value pairs and sub-documents.
+ *
+ * @param map source map containing key-value pairs and sub-documents.
+ * @return a new {@link Document}.
+ */
+ static Document from(Map map) {
+
+ Assert.notNull(map, "Map must not be null");
+
+ if (map instanceof LinkedHashMap) {
+ return new MapDocument(map);
+ }
+
+ return new MapDocument(new LinkedHashMap<>(map));
+ }
+
+ /**
+ * Parse JSON to {@link Document}.
+ *
+ * @param json must not be {@literal null}.
+ * @return the parsed {@link Document}.
+ */
+ static Document parse(String json) {
+ try {
+ return new MapDocument(MapDocument.OBJECT_MAPPER.readerFor(Map.class).readValue(json));
+ } catch (IOException e) {
+ throw new ElasticsearchException("Cannot parse JSON", e);
+ }
+ }
+
+ /**
+ * {@link #put(Object, Object)} the {@code key}/{@code value} tuple and return {@code this} {@link Document}.
+ *
+ * @param key key with which the specified value is to be associated.
+ * @param value value to be associated with the specified key.
+ * @return {@code this} {@link Document}.
+ */
+ default Document append(String key, Object value) {
+ put(key, value);
+ return this;
+ }
+
+ /**
+ * Return {@literal true} if this {@link Document} is associated with an identifier.
+ *
+ * @return {@literal true} if this {@link Document} is associated with an identifier, {@literal false} otherwise.
+ */
+ default boolean hasId() {
+ return false;
+ }
+
+ /**
+ * Retrieve the identifier associated with this {@link Document}.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}. It's recommended to check {@link #hasId()}
+ * prior to calling this method.
+ *
+ * @return the identifier associated with this {@link Document}.
+ * @throws IllegalStateException if the underlying implementation supports Id's but no Id was yet associated with the
+ * document.
+ */
+ default String getId() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Set the identifier for this {@link Document}.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ */
+ default void setId(String id) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Return {@literal true} if this {@link Document} is associated with a version.
+ *
+ * @return {@literal true} if this {@link Document} is associated with a version, {@literal false} otherwise.
+ */
+ default boolean hasVersion() {
+ return false;
+ }
+
+ /**
+ * Retrieve the version associated with this {@link Document}.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}. It's recommended to check
+ * {@link #hasVersion()} prior to calling this method.
+ *
+ * @return the version associated with this {@link Document}.
+ * @throws IllegalStateException if the underlying implementation supports Id's but no Id was yet associated with the
+ * document.
+ */
+ default long getVersion() {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Set the version for this {@link Document}.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ */
+ default void setVersion(long version) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
+ * mapping for the key. The value is casted within the method which makes it useful for calling code as it does not
+ * require casting on the calling side. If the value type is not assignable to {@code type}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @param type the expected return value type.
+ * @param expected return type.
+ * @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
+ * the key.
+ * @throws ClassCastException if the value of the given key is not of {@code type T}.
+ */
+ @Nullable
+ default T get(Object key, Class type) {
+
+ Assert.notNull(key, "Key must not be null");
+ Assert.notNull(type, "Type must not be null");
+
+ return type.cast(get(key));
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
+ * mapping for the key. If the value type is not a {@link Boolean}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
+ * the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Boolean}.
+ */
+ @Nullable
+ default Boolean getBoolean(String key) {
+ return get(key, Boolean.class);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
+ * mapping for the key. If the value type is not a {@link Boolean}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
+ * for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Boolean}.
+ */
+ default boolean getBooleanOrDefault(String key, boolean defaultValue) {
+ return getBooleanOrDefault(key, () -> defaultValue);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
+ * document contains no mapping for the key. If the value type is not a {@link Boolean}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
+ * contains no mapping for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Boolean}.
+ * @see BooleanSupplier
+ */
+ default boolean getBooleanOrDefault(String key, BooleanSupplier defaultValue) {
+
+ Boolean value = getBoolean(key);
+
+ return value == null ? defaultValue.getAsBoolean() : value;
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
+ * mapping for the key. If the value type is not a {@link Integer}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
+ * the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Integer}.
+ */
+ @Nullable
+ default Integer getInt(String key) {
+ return get(key, Integer.class);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
+ * mapping for the key. If the value type is not a {@link Integer}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
+ * for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Integer}.
+ */
+ default int getIntOrDefault(String key, int defaultValue) {
+ return getIntOrDefault(key, () -> defaultValue);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
+ * document contains no mapping for the key. If the value type is not a {@link Integer}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
+ * contains no mapping for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Integer}.
+ * @see IntSupplier
+ */
+ default int getIntOrDefault(String key, IntSupplier defaultValue) {
+
+ Integer value = getInt(key);
+
+ return value == null ? defaultValue.getAsInt() : value;
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
+ * mapping for the key. If the value type is not a {@link Long}, then this method throws {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
+ * the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Long}.
+ */
+ @Nullable
+ default Long getLong(String key) {
+ return get(key, Long.class);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
+ * mapping for the key. If the value type is not a {@link Long}, then this method throws {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
+ * for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Long}.
+ */
+ default long getLongOrDefault(String key, long defaultValue) {
+ return getLongOrDefault(key, () -> defaultValue);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
+ * document contains no mapping for the key. If the value type is not a {@link Long}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
+ * contains no mapping for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link Long}.
+ * @see LongSupplier
+ */
+ default long getLongOrDefault(String key, LongSupplier defaultValue) {
+
+ Long value = getLong(key);
+
+ return value == null ? defaultValue.getAsLong() : value;
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped, or {@literal null} if this document contains no
+ * mapping for the key. If the value type is not a {@link String}, then this method throws {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped, or {@literal null} if this document contains no mapping for
+ * the key.
+ * @throws ClassCastException if the value of the given key is not a {@link String}.
+ */
+ @Nullable
+ default String getString(String key) {
+ return get(key, String.class);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or {@code defaultValue} if this document contains no
+ * mapping for the key. If the value type is not a {@link String}, then this method throws {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or {@code defaultValue} if this document contains no mapping
+ * for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link String}.
+ */
+ default String getStringOrDefault(String key, String defaultValue) {
+ return getStringOrDefault(key, () -> defaultValue);
+ }
+
+ /**
+ * Returns the value to which the specified {@code key} is mapped or the value from {@code defaultValue} if this
+ * document contains no mapping for the key. If the value type is not a {@link String}, then this method throws
+ * {@link ClassCastException}.
+ *
+ * @param key the key whose associated value is to be returned
+ * @return the value to which the specified key is mapped or the value from {@code defaultValue} if this document
+ * contains no mapping for the key.
+ * @throws ClassCastException if the value of the given key is not a {@link String}.
+ * @see Supplier
+ */
+ default String getStringOrDefault(String key, Supplier defaultValue) {
+
+ String value = getString(key);
+
+ return value == null ? defaultValue.get() : value;
+ }
+
+ /**
+ * This method allows the application of a function to {@code this} {@link Document}. The function should expect a
+ * single {@link Document} argument and produce an {@code R} result.
+ *
+ * Any exception thrown by the function will be propagated to the caller.
+ *
+ * @param transformer functional interface to a apply
+ * @param class of the result
+ * @return the result of applying the function to this string
+ * @see java.util.function.Function
+ */
+ default R transform(Function super Document, ? extends R> transformer) {
+ return transformer.apply(this);
+ }
+
+ /**
+ * Render this {@link Document} to JSON. Auxiliary values such as Id and version are not considered within the JSON
+ * representation.
+ *
+ * @return a JSON representation of this document.
+ */
+ String toJson();
+}
diff --git a/src/main/java/org/springframework/data/elasticsearch/MapDocument.java b/src/main/java/org/springframework/data/elasticsearch/MapDocument.java
new file mode 100644
index 000000000..317a1894d
--- /dev/null
+++ b/src/main/java/org/springframework/data/elasticsearch/MapDocument.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright 2019 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 org.springframework.data.elasticsearch;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+import org.springframework.lang.Nullable;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * {@link Document} implementation backed by a {@link LinkedHashMap}.
+ *
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class MapDocument implements Document {
+
+ static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+ private final LinkedHashMap documentAsMap;
+
+ private @Nullable String id;
+ private @Nullable Long version;
+
+ MapDocument() {
+ this(new LinkedHashMap<>());
+ }
+
+ MapDocument(Map documentAsMap) {
+ this.documentAsMap = new LinkedHashMap<>(documentAsMap);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#hasId()
+ */
+ @Override
+ public boolean hasId() {
+ return this.id != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#getId()
+ */
+ @Override
+ public String getId() {
+
+ if (!hasId()) {
+ throw new IllegalStateException("No Id associated with this Document");
+ }
+
+ return this.id;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#setId(java.lang.String)
+ */
+ @Override
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#hasVersion()
+ */
+ @Override
+ public boolean hasVersion() {
+ return this.version != null;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#getVersion()
+ */
+ @Override
+ public long getVersion() {
+
+ if (!hasVersion()) {
+ throw new IllegalStateException("No version associated with this Document");
+ }
+
+ return this.version;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see org.springframework.data.elasticsearch.Document#setVersion(long)
+ */
+ @Override
+ public void setVersion(long version) {
+ this.version = version;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#size()
+ */
+ @Override
+ public int size() {
+ return documentAsMap.size();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#isEmpty()
+ */
+ @Override
+ public boolean isEmpty() {
+ return documentAsMap.isEmpty();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#containsKey(java.lang.Object)
+ */
+ @Override
+ public boolean containsKey(Object key) {
+ return documentAsMap.containsKey(key);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#containsValue(java.lang.Object)
+ */
+ @Override
+ public boolean containsValue(Object value) {
+ return documentAsMap.containsValue(value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#get(java.lang.Object)
+ */
+ @Override
+ public Object get(Object key) {
+ return documentAsMap.get(key);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.lang.Object#getOrDefault(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public Object getOrDefault(Object key, Object defaultValue) {
+ return documentAsMap.getOrDefault(key, defaultValue);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#put(java.lang.Object, java.lang.Object)
+ */
+ @Override
+ public Object put(String key, Object value) {
+ return documentAsMap.put(key, value);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#remove(java.lang.Object)
+ */
+ @Override
+ public Object remove(Object key) {
+ return documentAsMap.remove(key);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#putAll(Map)
+ */
+ @Override
+ public void putAll(Map extends String, ?> m) {
+ documentAsMap.putAll(m);
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#clear()
+ */
+ @Override
+ public void clear() {
+ documentAsMap.clear();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#keySet()
+ */
+ @Override
+ public Set keySet() {
+ return documentAsMap.keySet();
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see java.util.Map#values()
+ */
+ @Override
+ public Collection