Repository: apollographql/apollo-client

Problem statement:
Proposal: opening up the cache-inmemory implementation [apollo-client@2.0]
Hi apollo-client devs! Such an _amazing_ job with the 2.0 release so far, kudos. 💥 

We've decided to port our setup over to Apollo 💛, but our needs are such that we want to integrate it with our custom `Observable` cache for storing the normalized objects. 

The main problem is the way those normalized objects are currently stored and modified. Our implementation requires us to dispatch events whenever any object changes in the store, however that's currently impossible without actually forking the whole package.

The way objects are updated now is by modifying the `data` object directly -- and there is no way to hook into the `set` or `delete` events (without resolving to hacks like ES6 Proxies, which aren't supported everywhere yet).

The simplest solution to this is adding a Map-like API/abstraction over the [`data` property](https://github.com/apollographql/apollo-client/blob/489e4b74ea74f7d0884aaa049c909fb267086e20/packages/apollo-cache-inmemory/src/inMemoryCache.ts#L38), so that instead of working with a pure object, it would work with an API, like this:
- `this.data[dataId] = x` => `this.data.set(dataId, x)`
- `this.data[dataId]` => `this.data.get(dataId)`
- `delete this.data[dataId]` => `this.data.delete(dataId)`
- `this.data = {}` => `this.data.clear()`
- `this.data` => `this.data.toObject()` (extra method that flattens the contents to a plain object)

This way `data` can be a single instance throughout the whole life of the `InMemoryCache` and its underlying implementation can vary -- i.e. it can be an `Object`, as it is now, or a native `Map` (which is generally faster than `object[key]` operations).

A similar abstraction could be put in place of the `optimistic` property, currently an `Array` instance, but it could have a simple, `Set`-like interface.

I have the first change ready in a fork (it's a simple change, though in many places), with all the tests passing, and I've also added some extra ones for the added abstraction. This feature would greatly simplify our setup, since we would not need to fork the whole package, but only provide a custom implementation of the Cache when it is constructed.

If you are happy with this proposal, I'll refactor my fork, cleanup the commits and open a PR 🥇.

Also, we'd be happy to share our Observable-based implementation of the cache with the Apollo OSS community, once it's ready.

**Intended outcome:**
Able to hook the changes to cache, while re-using 99% of the `apollo-cache-inmemory` without creating a fork.

This requires adding two simple abstractions:
1. over the `data` property [defined here](https://github.com/apollographql/apollo-client/blob/489e4b74ea74f7d0884aaa049c909fb267086e20/packages/apollo-cache-inmemory/src/inMemoryCache.ts#L38)
2. over the `optimistic` property [defined here](https://github.com/apollographql/apollo-client/blob/489e4b74ea74f7d0884aaa049c909fb267086e20/packages/apollo-cache-inmemory/src/inMemoryCache.ts#L40)

**Actual outcome:**

Fork was required. :)

**Version**
- apollo-cache-inmemory@0.2.0-beta.4



Hints:
I'm all for this! The external API provided by Apollo Cache should be able to stay the same and the internals change as needed! Let's try it out!
@niieani @jbaxleyiii If we would abstract data's setter and getter it would be super easy to use normalized cache in `@ngrx/store` :ng: :+1: . 
I forked Apollo's `InMemoryCache` few days ago and created a working prototype based on it.

Would be awesome to make it till the official release :heart: 
@niieani any progress on that?

Patch:
diff --git a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/fragmentMatcher.ts
--- a/packages/apollo-cache-inmemory/src/fragmentMatcher.ts
+++ b/packages/apollo-cache-inmemory/src/fragmentMatcher.ts
@@ -30,7 +30,7 @@ export class HeuristicFragmentMatcher implements FragmentMatcherInterface {
     typeCondition: string,
     context: ReadStoreContext,
   ): boolean {
-    const obj = context.store[idValue.id];
+    const obj = context.store.get(idValue.id);
 
     if (!obj) {
       return false;
@@ -116,7 +116,7 @@ export class IntrospectionFragmentMatcher implements FragmentMatcherInterface {
       );
     }
 
-    const obj = context.store[idValue.id];
+    const obj = context.store.get(idValue.id);
 
     if (!obj) {
       return false;
diff --git a/packages/apollo-cache-inmemory/src/inMemoryCache.ts b/packages/apollo-cache-inmemory/src/inMemoryCache.ts
--- a/packages/apollo-cache-inmemory/src/inMemoryCache.ts
+++ b/packages/apollo-cache-inmemory/src/inMemoryCache.ts
@@ -12,14 +12,18 @@ import {
   OptimisticStoreItem,
   ApolloReducerConfig,
   NormalizedCache,
+  NormalizedCacheObject,
 } from './types';
 import { writeResultToStore } from './writeToStore';
 import { readQueryFromStore, diffQueryAgainstStore } from './readFromStore';
+import { defaultNormalizedCacheFactory } from './objectCache';
+import { record } from './recordingCache';
 
 const defaultConfig: ApolloReducerConfig = {
   fragmentMatcher: new HeuristicFragmentMatcher(),
   dataIdFromObject: defaultDataIdFromObject,
   addTypename: true,
+  storeFactory: defaultNormalizedCacheFactory,
 };
 
 export function defaultDataIdFromObject(result: any): string | null {
@@ -34,8 +38,8 @@ export function defaultDataIdFromObject(result: any): string | null {
   return null;
 }
 
-export class InMemoryCache extends ApolloCache<NormalizedCache> {
-  private data: NormalizedCache = {};
+export class InMemoryCache extends ApolloCache<NormalizedCacheObject> {
+  private data: NormalizedCache;
   private config: ApolloReducerConfig;
   private optimistic: OptimisticStoreItem[] = [];
   private watches: Cache.WatchOptions[] = [];
@@ -52,29 +56,30 @@ export class InMemoryCache extends ApolloCache<NormalizedCache> {
     if ((this.config as any).customResolvers)
       this.config.cacheResolvers = (this.config as any).customResolvers;
     this.addTypename = this.config.addTypename ? true : false;
+    this.data = this.config.storeFactory();
   }
 
-  public restore(data: NormalizedCache): ApolloCache<NormalizedCache> {
-    if (data) this.data = data;
+  public restore(data: NormalizedCacheObject): this {
+    if (data) this.data.replace(data);
     return this;
   }
 
-  public extract(optimistic: boolean = false): NormalizedCache {
+  public extract(optimistic: boolean = false): NormalizedCacheObject {
     if (optimistic && this.optimistic.length > 0) {
       const patches = this.optimistic.map(opt => opt.data);
-      return Object.assign({}, this.data, ...patches) as NormalizedCache;
+      return Object.assign(this.data.toObject(), ...patches);
     }
 
-    return this.data;
+    return this.data.toObject();
   }
 
-  public read<T>(query: Cache.ReadOptions): T {
-    if (query.rootId && typeof this.data[query.rootId] === 'undefined') {
+  public read<T>(query: Cache.ReadOptions): T | null {
+    if (query.rootId && this.data.get(query.rootId) === undefined) {
       return null;
     }
 
     return readQueryFromStore({
-      store: this.extract(query.optimistic),
+      store: this.config.storeFactory(this.extract(query.optimistic)),
       query: this.transformDocument(query.query),
       variables: query.variables,
       rootId: query.rootId,
@@ -100,7 +105,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCache> {
 
   public diff<T>(query: Cache.DiffOptions): Cache.DiffResult<T> {
     return diffQueryAgainstStore({
-      store: this.extract(query.optimistic),
+      store: this.config.storeFactory(this.extract(query.optimistic)),
       query: this.transformDocument(query.query),
       variables: query.variables,
       returnPartialData: query.returnPartialData,
@@ -123,7 +128,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCache> {
   }
 
   public reset(): Promise<void> {
-    this.data = {};
+    this.data.clear();
     this.broadcastWatches();
 
     return Promise.resolve();
@@ -143,7 +148,7 @@ export class InMemoryCache extends ApolloCache<NormalizedCache> {
     this.broadcastWatches();
   }
 
-  public performTransaction(transaction: Transaction<NormalizedCache>) {
+  public performTransaction(transaction: Transaction<NormalizedCacheObject>) {
     // TODO: does this need to be different, or is this okay for an in-memory cache?
 
     let alreadySilenced = this.silenceBroadcast;
@@ -161,25 +166,18 @@ export class InMemoryCache extends ApolloCache<NormalizedCache> {
   }
 
   public recordOptimisticTransaction(
-    transaction: Transaction<NormalizedCache>,
+    transaction: Transaction<NormalizedCacheObject>,
     id: string,
   ) {
     this.silenceBroadcast = true;
 
-    const before = this.extract(true);
-
-    const orig = this.data;
-    this.data = { ...before };
-    this.performTransaction(transaction);
-    const after = this.data;
-    this.data = orig;
-
-    const patch: any = {};
-
-    Object.keys(after).forEach(key => {
-      if (after[key] !== before[key]) {
-        patch[key] = after[key];
-      }
+    const patch = record(this.extract(true), recordingCache => {
+      // swapping data instance on 'this' is currently necessary
+      // because of the current architecture
+      const dataCache = this.data;
+      this.data = recordingCache;
+      this.performTransaction(transaction);
+      this.data = dataCache;
     });
 
     this.optimistic.push({
diff --git a/packages/apollo-cache-inmemory/src/index.ts b/packages/apollo-cache-inmemory/src/index.ts
--- a/packages/apollo-cache-inmemory/src/index.ts
+++ b/packages/apollo-cache-inmemory/src/index.ts
@@ -2,4 +2,6 @@ export { InMemoryCache, defaultDataIdFromObject } from './inMemoryCache';
 export * from './readFromStore';
 export * from './writeToStore';
 export * from './fragmentMatcher';
+export * from './objectCache';
+export * from './recordingCache';
 export * from './types';
diff --git a/packages/apollo-cache-inmemory/src/mapCache.ts b/packages/apollo-cache-inmemory/src/mapCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/mapCache.ts
@@ -0,0 +1,47 @@
+import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
+
+function getNormalizedDataId(dataId: string | number): string {
+  return typeof dataId === 'number' ? String(dataId) : dataId;
+}
+
+/**
+ * A Map-based implementation of the NormalizedCache.
+ * Note that you need a polyfill for Object.entries for this to work.
+ */
+export class MapCache implements NormalizedCache {
+  cache: Map<string, StoreObject>;
+  constructor(data: NormalizedCacheObject = {}) {
+    this.cache = new Map(Object.entries(data));
+  }
+  get(dataId: string): StoreObject {
+    return this.cache.get(getNormalizedDataId(dataId));
+  }
+  set(dataId: string, value: StoreObject): void {
+    this.cache.set(getNormalizedDataId(dataId), value);
+  }
+  delete(dataId: string): void {
+    this.cache.delete(getNormalizedDataId(dataId));
+  }
+  clear(): void {
+    return this.cache.clear();
+  }
+  public toObject(): NormalizedCacheObject {
+    const obj: NormalizedCacheObject = {};
+    this.cache.forEach((dataId, key) => {
+      obj[key] = dataId;
+    });
+    return obj;
+  }
+  public replace(newData: NormalizedCacheObject): void {
+    this.cache.clear();
+    Object.entries(newData).forEach(([dataId, value]) =>
+      this.cache.set(dataId, value),
+    );
+  }
+}
+
+export function mapNormalizedCacheFactory(
+  seed?: NormalizedCacheObject,
+): NormalizedCache {
+  return new MapCache(seed);
+}
diff --git a/packages/apollo-cache-inmemory/src/objectCache.ts b/packages/apollo-cache-inmemory/src/objectCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/objectCache.ts
@@ -0,0 +1,29 @@
+import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
+
+export class ObjectCache implements NormalizedCache {
+  constructor(private data: NormalizedCacheObject = {}) {}
+  public toObject(): NormalizedCacheObject {
+    return { ...this.data };
+  }
+  public get(dataId: string): StoreObject {
+    return this.data[dataId];
+  }
+  public set(dataId: string, value: StoreObject) {
+    this.data[dataId] = value;
+  }
+  public delete(dataId: string): void {
+    this.data[dataId] = undefined;
+  }
+  public clear(): void {
+    this.data = {};
+  }
+  public replace(newData: NormalizedCacheObject): void {
+    this.data = newData || {};
+  }
+}
+
+export function defaultNormalizedCacheFactory(
+  seed?: NormalizedCacheObject,
+): NormalizedCache {
+  return new ObjectCache(seed);
+}
diff --git a/packages/apollo-cache-inmemory/src/readFromStore.ts b/packages/apollo-cache-inmemory/src/readFromStore.ts
--- a/packages/apollo-cache-inmemory/src/readFromStore.ts
+++ b/packages/apollo-cache-inmemory/src/readFromStore.ts
@@ -65,7 +65,7 @@ const readStoreResolver: Resolver = (
   assertIdValue(idValue);
 
   const objId = idValue.id;
-  const obj = context.store[objId];
+  const obj = context.store.get(objId);
   const storeKeyName = getStoreKeyName(fieldName, args, directives);
   let fieldValue = (obj || {})[storeKeyName];
 
diff --git a/packages/apollo-cache-inmemory/src/recordingCache.ts b/packages/apollo-cache-inmemory/src/recordingCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/recordingCache.ts
@@ -0,0 +1,56 @@
+import { NormalizedCache, NormalizedCacheObject, StoreObject } from './types';
+
+export class RecordingCache implements NormalizedCache {
+  constructor(private readonly data: NormalizedCacheObject = {}) {}
+
+  private recordedData: NormalizedCacheObject = {};
+
+  public record(
+    transaction: (recordingCache: RecordingCache) => void,
+  ): NormalizedCacheObject {
+    transaction(this);
+    const recordedData = this.recordedData;
+    this.recordedData = {};
+    return recordedData;
+  }
+
+  public toObject(): NormalizedCacheObject {
+    return { ...this.data, ...this.recordedData };
+  }
+
+  public get(dataId: string): StoreObject {
+    if (this.recordedData.hasOwnProperty(dataId)) {
+      // recording always takes precedence:
+      return this.recordedData[dataId];
+    }
+    return this.data[dataId];
+  }
+
+  public set(dataId: string, value: StoreObject) {
+    if (this.get(dataId) !== value) {
+      this.recordedData[dataId] = value;
+    }
+  }
+
+  public delete(dataId: string): void {
+    this.recordedData[dataId] = undefined;
+  }
+
+  public clear(): void {
+    Object.keys(this.data).forEach(dataId => this.delete(dataId));
+    this.recordedData = {};
+  }
+
+  public replace(newData: NormalizedCacheObject): void {
+    this.clear();
+    this.recordedData = { ...newData };
+  }
+}
+
+export function record(
+  startingState: NormalizedCacheObject,
+  transaction: (recordingCache: RecordingCache) => void,
+): NormalizedCacheObject {
+  const recordingCache = new RecordingCache(startingState);
+  return recordingCache.record(transaction);
+}
diff --git a/packages/apollo-cache-inmemory/src/types.ts b/packages/apollo-cache-inmemory/src/types.ts
--- a/packages/apollo-cache-inmemory/src/types.ts
+++ b/packages/apollo-cache-inmemory/src/types.ts
@@ -1,22 +1,53 @@
 import { DocumentNode } from 'graphql';
 import { FragmentMatcher } from 'graphql-anywhere';
 import { Transaction } from 'apollo-cache';
-import { StoreValue, IdValue } from 'apollo-utilities';
+import { IdValue, StoreValue } from 'apollo-utilities';
+import { NormalizedCacheObject, NormalizedCache } from './types';
 
 export type IdGetter = (value: Object) => string | null | undefined;
 
+/**
+ * This is an interface used to access, set and remove
+ * StoreObjects from the cache
+ */
+export interface NormalizedCache {
+  get(dataId: string): StoreObject;
+  set(dataId: string, value: StoreObject): void;
+  delete(dataId: string): void;
+  clear(): void;
+
+  // non-Map elements:
+  /**
+   * returns an Object with key-value pairs matching the contents of the store
+   */
+  toObject(): NormalizedCacheObject;
+  /**
+   * replace the state of the store
+   */
+  replace(newData: NormalizedCacheObject): void;
+}
+
 /**
  * This is a normalized representation of the Apollo query result cache. It consists of
  * a flattened representation of query result trees.
  */
-export interface NormalizedCache {
+export interface NormalizedCacheObject {
   [dataId: string]: StoreObject;
 }
 
+export interface StoreObject {
+  __typename?: string;
+  [storeFieldKey: string]: StoreValue;
+}
+
+export type NormalizedCacheFactory = (
+  seed?: NormalizedCacheObject,
+) => NormalizedCache;
+
 export type OptimisticStoreItem = {
   id: string;
-  data: NormalizedCache;
-  transaction: Transaction<NormalizedCache>;
+  data: NormalizedCacheObject;
+  transaction: Transaction<NormalizedCacheObject>;
 };
 
 export type ReadQueryOptions = {
@@ -33,16 +64,12 @@ export type DiffQueryAgainstStoreOptions = ReadQueryOptions & {
   returnPartialData?: boolean;
 };
 
-export interface StoreObject {
-  __typename?: string;
-  [storeFieldKey: string]: StoreValue;
-}
-
 export type ApolloReducerConfig = {
   dataIdFromObject?: IdGetter;
   fragmentMatcher?: FragmentMatcherInterface;
   addTypename?: boolean;
   cacheResolvers?: CacheResolverMap;
+  storeFactory?: NormalizedCacheFactory;
 };
 
 export type ReadStoreContext = {
diff --git a/packages/apollo-cache-inmemory/src/writeToStore.ts b/packages/apollo-cache-inmemory/src/writeToStore.ts
--- a/packages/apollo-cache-inmemory/src/writeToStore.ts
+++ b/packages/apollo-cache-inmemory/src/writeToStore.ts
@@ -26,9 +26,12 @@ import {
   getQueryDefinition,
 } from 'apollo-utilities';
 
+import { defaultNormalizedCacheFactory, ObjectCache } from './objectCache';
+
 import {
   IdGetter,
   NormalizedCache,
+  NormalizedCacheFactory,
   ReadStoreContext,
   StoreObject,
 } from './types';
@@ -72,7 +75,8 @@ export function enhanceErrorWithDocument(error: Error, document: DocumentNode) {
 export function writeQueryToStore({
   result,
   query,
-  store = {} as NormalizedCache,
+  storeFactory = defaultNormalizedCacheFactory,
+  store = storeFactory(),
   variables,
   dataIdFromObject,
   fragmentMap = {} as FragmentMap,
@@ -81,6 +85,7 @@ export function writeQueryToStore({
   result: Object;
   query: DocumentNode;
   store?: NormalizedCache;
+  storeFactory?: NormalizedCacheFactory;
   variables?: Object;
   dataIdFromObject?: IdGetter;
   fragmentMap?: FragmentMap;
@@ -97,6 +102,7 @@ export function writeQueryToStore({
       selectionSet: queryDefinition.selectionSet,
       context: {
         store,
+        storeFactory,
         processedData: {},
         variables,
         dataIdFromObject,
@@ -111,6 +117,7 @@ export function writeQueryToStore({
 
 export type WriteContext = {
   store: NormalizedCache;
+  storeFactory: NormalizedCacheFactory;
   processedData?: { [x: string]: FieldNode[] };
   variables?: any;
   dataIdFromObject?: IdGetter;
@@ -122,7 +129,8 @@ export function writeResultToStore({
   dataId,
   result,
   document,
-  store = {} as NormalizedCache,
+  storeFactory = defaultNormalizedCacheFactory,
+  store = storeFactory(),
   variables,
   dataIdFromObject,
   fragmentMatcherFunction,
@@ -131,6 +139,7 @@ export function writeResultToStore({
   result: any;
   document: DocumentNode;
   store?: NormalizedCache;
+  storeFactory?: NormalizedCacheFactory;
   variables?: Object;
   dataIdFromObject?: IdGetter;
   fragmentMatcherFunction?: FragmentMatcher;
@@ -149,6 +158,7 @@ export function writeResultToStore({
       selectionSet,
       context: {
         store,
+        storeFactory,
         processedData: {},
         variables,
         dataIdFromObject,
@@ -236,7 +246,9 @@ export function writeSelectionSetToStore({
         // on the context.
         const idValue: IdValue = { type: 'id', id: 'self', generated: false };
         const fakeContext: ReadStoreContext = {
-          store: { self: result },
+          // NOTE: fakeContext always uses ObjectCache
+          // since this is only to ensure the return value of 'matches'
+          store: new ObjectCache({ self: result }),
           returnPartialData: false,
           hasMissingField: false,
           cacheResolvers: {},
@@ -276,8 +288,8 @@ function mergeWithGenerated(
   realKey: string,
   cache: NormalizedCache,
 ) {
-  const generated = cache[generatedKey];
-  const real = cache[realKey];
+  const generated = cache.get(generatedKey);
+  const real = cache.get(realKey);
 
   Object.keys(generated).forEach(key => {
     const value = generated[key];
@@ -285,8 +297,8 @@ function mergeWithGenerated(
     if (isIdValue(value) && isGeneratedId(value.id) && isIdValue(realValue)) {
       mergeWithGenerated(value.id, realValue.id, cache);
     }
-    delete cache[generatedKey];
-    cache[realKey] = { ...generated, ...real } as StoreObject;
+    cache.delete(generatedKey);
+    cache.set(realKey, { ...generated, ...real } as StoreObject);
   });
 }
 
@@ -326,6 +338,7 @@ function writeFieldToStore({
   const { variables, dataIdFromObject, store } = context;
 
   let storeValue: any;
+  let storeObject: StoreObject;
 
   const storeFieldName: string = storeKeyNameFromField(field, variables);
   // specifies if we need to merge existing keys in the store
@@ -401,8 +414,9 @@ function writeFieldToStore({
     // check if there was a generated id at the location where we're
     // about to place this new id. If there was, we have to merge the
     // data from that id with the data we're about to write in the store.
-    if (store[dataId] && store[dataId][storeFieldName] !== storeValue) {
-      const escapedId = store[dataId][storeFieldName] as IdValue;
+    storeObject = store.get(dataId);
+    if (storeObject && storeObject[storeFieldName] !== storeValue) {
+      const escapedId = storeObject[storeFieldName] as IdValue;
 
       // If there is already a real id in the store and the current id we
       // are dealing with is generated, we throw an error.
@@ -426,7 +440,7 @@ function writeFieldToStore({
   }
 
   const newStoreObj = {
-    ...store[dataId],
+    ...store.get(dataId),
     [storeFieldName]: storeValue,
   } as StoreObject;
 
@@ -434,8 +448,9 @@ function writeFieldToStore({
     mergeWithGenerated(generatedKey, (storeValue as IdValue).id, store);
   }
 
-  if (!store[dataId] || storeValue !== store[dataId][storeFieldName]) {
-    store[dataId] = newStoreObj;
+  storeObject = store.get(dataId);
+  if (!storeObject || storeValue !== storeObject[storeFieldName]) {
+    store.set(dataId, newStoreObj);
   }
 }
 
diff --git a/packages/apollo-cache/src/cache.ts b/packages/apollo-cache/src/cache.ts
--- a/packages/apollo-cache/src/cache.ts
+++ b/packages/apollo-cache/src/cache.ts
@@ -30,7 +30,7 @@ export abstract class ApolloCache<TSerialized> implements DataProxy {
   /**
    * Exposes the cache's complete state, in a serializable format for later restoration.
    */
-  public abstract extract(optimistic: boolean): TSerialized;
+  public abstract extract(optimistic?: boolean): TSerialized;
 
   // optimistic API
   public abstract removeOptimistic(id: string): void;


Test patch:
diff --git a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/diffAgainstStore.ts
@@ -95,7 +95,7 @@ describe('diffing queries against the store', () => {
     });
 
     expect(complete).toBeTruthy();
-    expect(store['1']).toEqual(result.people_one);
+    expect(store.get('1')).toEqual(result.people_one);
   });
 
   it('does not swallow errors other than field errors', () => {
diff --git a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts b/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/fragmentMatcher.ts
@@ -1,4 +1,6 @@
 import { IntrospectionFragmentMatcher } from '../fragmentMatcher';
+import { defaultNormalizedCacheFactory } from '../objectCache';
+
 describe('IntrospectionFragmentMatcher', () => {
   it('will throw an error if match is called if it is not ready', () => {
     const ifm = new IntrospectionFragmentMatcher();
@@ -27,11 +29,11 @@ describe('IntrospectionFragmentMatcher', () => {
       },
     });
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       a: {
         __typename: 'ItemB',
       },
-    };
+    });
 
     const idValue = {
       type: 'id',
diff --git a/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts b/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/__tests__/mapCache.ts
@@ -0,0 +1,20 @@
+jest.mock('../objectCache', () => {
+  const { MapCache, mapNormalizedCacheFactory } = require('../mapCache');
+  return {
+    ObjectCache: MapCache,
+    defaultNormalizedCacheFactory: mapNormalizedCacheFactory,
+  };
+});
+
+describe('MapCache', () => {
+  // simply re-runs all the tests
+  // with the alternative implementation of the cache
+  require('./objectCache');
+  require('./cache');
+  require('./diffAgainstStore');
+  require('./fragmentMatcher');
+  require('./readFromStore');
+  require('./diffAgainstStore');
+  require('./roundtrip');
+  require('./writeToStore');
+});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts b/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/__tests__/objectCache.ts
@@ -0,0 +1,36 @@
+import { ObjectCache } from '../objectCache';
+import { NormalizedCacheObject } from '../types';
+
+describe('ObjectCache', () => {
+  it('should create an empty cache', () => {
+    const cache = new ObjectCache();
+    expect(cache.toObject()).toEqual({});
+  });
+
+  it('should create a cache based on an Object', () => {
+    const contents: NormalizedCacheObject = { a: {} };
+    const cache = new ObjectCache(contents);
+    expect(cache.toObject()).toEqual(contents);
+  });
+
+  it(`should .get() an object from the store by dataId`, () => {
+    const contents: NormalizedCacheObject = { a: {} };
+    const cache = new ObjectCache(contents);
+    expect(cache.get('a')).toBe(contents.a);
+  });
+
+  it(`should .set() an object from the store by dataId`, () => {
+    const obj = {};
+    const cache = new ObjectCache();
+    cache.set('a', obj);
+    expect(cache.get('a')).toBe(obj);
+  });
+
+  it(`should .clear() the store`, () => {
+    const obj = {};
+    const cache = new ObjectCache();
+    cache.set('a', obj);
+    cache.clear();
+    expect(cache.get('a')).toBeUndefined();
+  });
+});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/readFromStore.ts
@@ -4,6 +4,7 @@ import gql from 'graphql-tag';
 
 import { NormalizedCache, StoreObject, HeuristicFragmentMatcher } from '../';
 import { readQueryFromStore } from '../readFromStore';
+import { defaultNormalizedCacheFactory } from '../objectCache';
 
 const fragmentMatcherFunction = new HeuristicFragmentMatcher().match;
 import { withError } from './diffAgainstStore';
@@ -11,7 +12,7 @@ import { withError } from './diffAgainstStore';
 describe('reading from the store', () => {
   it('runs a nested query with proper fragment fields in arrays', () => {
     withError(() => {
-      const store = {
+      const store = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
           __typename: 'Query',
           nestedObj: { type: 'id', id: 'abcde', generated: false },
@@ -26,7 +27,7 @@ describe('reading from the store', () => {
           id: 'abcdef',
           someField: 3,
         } as StoreObject,
-      } as NormalizedCache;
+      });
 
       const queryResult = readQueryFromStore({
         store,
@@ -71,7 +72,7 @@ describe('reading from the store', () => {
   it('rejects malformed queries', () => {
     expect(() => {
       readQueryFromStore({
-        store: {},
+        store: defaultNormalizedCacheFactory(),
         query: gql`
           query {
             name
@@ -86,7 +87,7 @@ describe('reading from the store', () => {
 
     expect(() => {
       readQueryFromStore({
-        store: {},
+        store: defaultNormalizedCacheFactory(),
         query: gql`
           fragment x on y {
             name
@@ -104,9 +105,9 @@ describe('reading from the store', () => {
       nullField: null,
     } as StoreObject;
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: result,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -141,14 +142,14 @@ describe('reading from the store', () => {
       stringArg: 'This is a string!',
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: {
         id: 'abcd',
         nullField: null,
         'numberField({"intArg":5,"floatArg":3.14})': 5,
         'stringField({"arg":"This is a string!"})': 'Heyo',
       },
-    } as NormalizedCache;
+    });
 
     const result = readQueryFromStore({
       store,
@@ -182,14 +183,14 @@ describe('reading from the store', () => {
       floatArg: 3.14,
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: {
         id: 'abcd',
         nullField: null,
         'numberField({"intArg":0,"floatArg":3.14})': 5,
         'stringField({"arg":"This is a default string!"})': 'Heyo',
       },
-    } as NormalizedCache;
+    });
 
     const result = readQueryFromStore({
       store,
@@ -219,7 +220,7 @@ describe('reading from the store', () => {
       } as StoreObject,
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
         nestedObj: {
           type: 'id',
@@ -228,7 +229,7 @@ describe('reading from the store', () => {
         },
       } as StoreObject),
       abcde: result.nestedObj,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -276,7 +277,7 @@ describe('reading from the store', () => {
       __typename: 'Item',
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign(
         {},
         assign({}, omit(result, 'nestedObj', 'deepNestedObj')),
@@ -297,7 +298,7 @@ describe('reading from the store', () => {
         },
       }) as StoreObject,
       abcdef: result.deepNestedObj as StoreObject,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -372,7 +373,7 @@ describe('reading from the store', () => {
       ] as StoreObject[],
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [
           { type: 'id', generated: true, id: 'abcd.nestedArray.0' } as IdValue,
@@ -381,7 +382,7 @@ describe('reading from the store', () => {
       }) as StoreObject,
       'abcd.nestedArray.0': result.nestedArray[0],
       'abcd.nestedArray.1': result.nestedArray[1],
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -430,7 +431,7 @@ describe('reading from the store', () => {
       ] as StoreObject[],
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [
           null,
@@ -438,7 +439,7 @@ describe('reading from the store', () => {
         ],
       }) as StoreObject,
       'abcd.nestedArray.1': result.nestedArray[1],
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -485,12 +486,12 @@ describe('reading from the store', () => {
       ] as StoreObject[],
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [null, { type: 'id', generated: false, id: 'abcde' }],
       }) as StoreObject,
       abcde: result.nestedArray[1],
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -530,7 +531,7 @@ describe('reading from the store', () => {
       nullField: null,
     } as StoreObject;
 
-    const store = { ROOT_QUERY: result } as NormalizedCache;
+    const store = defaultNormalizedCacheFactory({ ROOT_QUERY: result });
 
     expect(() => {
       readQueryFromStore({
@@ -554,11 +555,11 @@ describe('reading from the store', () => {
       nestedObj: null,
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
         nestedObj: null,
       }) as StoreObject,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -591,14 +592,14 @@ describe('reading from the store', () => {
       simpleArray: ['one', 'two', 'three'],
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
         simpleArray: {
           type: 'json',
           json: result.simpleArray,
         } as JsonValue,
       }) as StoreObject,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -628,14 +629,14 @@ describe('reading from the store', () => {
       simpleArray: [null, 'two', 'three'],
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'simpleArray')), {
         simpleArray: {
           type: 'json',
           json: result.simpleArray,
         } as JsonValue,
       }) as StoreObject,
-    } as NormalizedCache;
+    });
 
     const queryResult = readQueryFromStore({
       store,
@@ -677,7 +678,7 @@ describe('reading from the store', () => {
       __typename: 'Item',
     };
 
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign(
         {},
         assign({}, omit(data, 'nestedObj', 'deepNestedObj')),
@@ -698,7 +699,7 @@ describe('reading from the store', () => {
         },
       }) as StoreObject,
       abcdef: data.deepNestedObj as StoreObject,
-    } as NormalizedCache;
+    });
 
     const queryResult1 = readQueryFromStore({
       store,
@@ -748,7 +749,7 @@ describe('reading from the store', () => {
   });
 
   it('properly handles the connection directive', () => {
-    const store: NormalizedCache = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: {
         abc: [
           {
@@ -761,7 +762,7 @@ describe('reading from the store', () => {
       'ROOT_QUERY.abc.0': {
         name: 'efgh',
       },
-    };
+    });
 
     const queryResult = readQueryFromStore({
       store,
diff --git a/packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts b/packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts
new file mode 100644
--- /dev/null
+++ b/packages/apollo-cache-inmemory/src/__tests__/recordingCache.ts
@@ -0,0 +1,76 @@
+import { RecordingCache } from '../recordingCache';
+import { NormalizedCacheObject } from '../types';
+
+describe('RecordingCache', () => {
+  describe('returns correct values during recording', () => {
+    const data = {
+      Human: { __typename: 'Human', name: 'Mark' },
+      Animal: { __typename: 'Mouse', name: '🐭' },
+    };
+    const dataToRecord = { Human: { __typename: 'Human', name: 'John' } };
+    let cache: RecordingCache;
+
+    beforeEach(() => {
+      cache = new RecordingCache({ ...data });
+    });
+
+    it('should passthrough values if not defined in recording', () => {
+      cache.record(() => {
+        expect(cache.get('Human')).toBe(data.Human);
+        expect(cache.get('Animal')).toBe(data.Animal);
+      });
+    });
+
+    it('should return values defined during recording', () => {
+      const recording = cache.record(() => {
+        cache.set('Human', dataToRecord.Human);
+        expect(cache.get('Human')).toBe(dataToRecord.Human);
+      });
+      expect(recording.Human).toBe(dataToRecord.Human);
+    });
+
+    it('should return undefined for values deleted during recording', () => {
+      const recording = cache.record(() => {
+        expect(cache.get('Animal')).toBe(data.Animal);
+        // delete should be registered in the recording:
+        cache.delete('Animal');
+        expect(cache.get('Animal')).toBeUndefined();
+      });
+
+      expect(recording).toHaveProperty('Animal');
+    });
+  });
+
+  describe('returns correct result of a recorded transaction', () => {
+    const data = {
+      Human: { __typename: 'Human', name: 'Mark' },
+      Animal: { __typename: 'Mouse', name: '🐭' },
+    };
+    const dataToRecord = { Human: { __typename: 'Human', name: 'John' } };
+    let cache: RecordingCache;
+    let recording: NormalizedCacheObject;
+
+    beforeEach(() => {
+      cache = new RecordingCache({ ...data });
+      recording = cache.record(() => {
+        cache.set('Human', dataToRecord.Human);
+        cache.delete('Animal');
+      });
+    });
+
+    it('should contain the property indicating deletion', () => {
+      expect(recording).toHaveProperty('Animal');
+    });
+
+    it('should have recorded the changes made during recording', () => {
+      expect(recording).toEqual({
+        Human: dataToRecord.Human,
+        Animal: undefined,
+      });
+    });
+
+    it('should keep the original data unaffected', () => {
+      expect(cache.toObject()).toEqual(data);
+    });
+  });
+});
diff --git a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts
--- a/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts
+++ b/packages/apollo-cache-inmemory/src/__tests__/writeToStore.ts
@@ -21,6 +21,8 @@ import {
   writeSelectionSetToStore,
 } from '../writeToStore';
 
+import { defaultNormalizedCacheFactory } from '../objectCache';
+
 import {
   HeuristicFragmentMatcher,
   IntrospectionFragmentMatcher,
@@ -65,7 +67,7 @@ describe('writing to the store', () => {
       writeQueryToStore({
         query,
         result: cloneDeep(result),
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: result,
     });
@@ -93,7 +95,7 @@ describe('writing to the store', () => {
       query,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'abcd',
         stringField: 'This is a string!',
@@ -127,7 +129,7 @@ describe('writing to the store', () => {
       query,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'abcd',
         'stringField({"arg":1})': 'The arg was 1!',
@@ -167,7 +169,7 @@ describe('writing to the store', () => {
       variables,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'abcd',
         nullField: null,
@@ -209,7 +211,7 @@ describe('writing to the store', () => {
       variables,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'abcd',
         nullField: null,
@@ -253,7 +255,7 @@ describe('writing to the store', () => {
         query,
         result: cloneDeep(result),
         dataIdFromObject: getIdField,
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign<{}>({}, assign({}, omit(result, 'nestedObj')), {
         nestedObj: {
@@ -297,7 +299,7 @@ describe('writing to the store', () => {
       writeQueryToStore({
         query,
         result: cloneDeep(result),
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
         nestedObj: {
@@ -341,7 +343,7 @@ describe('writing to the store', () => {
       writeQueryToStore({
         query,
         result: cloneDeep(result),
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
         'nestedObj({"arg":"val"})': {
@@ -396,7 +398,7 @@ describe('writing to the store', () => {
         query,
         result: cloneDeep(result),
         dataIdFromObject: getIdField,
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: result.nestedArray.map((obj: any) => ({
@@ -447,7 +449,7 @@ describe('writing to the store', () => {
         query,
         result: cloneDeep(result),
         dataIdFromObject: getIdField,
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign<{}>({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [
@@ -498,7 +500,7 @@ describe('writing to the store', () => {
       result: cloneDeep(result),
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [
           { type: 'id', generated: true, id: `ROOT_QUERY.nestedArray.0` },
@@ -545,7 +547,7 @@ describe('writing to the store', () => {
       result: cloneDeep(result),
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedArray')), {
         nestedArray: [
           null,
@@ -581,7 +583,7 @@ describe('writing to the store', () => {
       dataIdFromObject: getIdField,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: assign<{}>({}, assign({}, omit(result, 'simpleArray')), {
         simpleArray: {
           type: 'json',
@@ -619,7 +621,7 @@ describe('writing to the store', () => {
       result: cloneDeep(result),
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: assign<{}>({}, assign({}, omit(result, 'simpleArray')), {
         simpleArray: {
           type: 'json',
@@ -666,7 +668,7 @@ describe('writing to the store', () => {
       dataIdFromObject: getIdField,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'a',
         object1: {
@@ -741,7 +743,7 @@ describe('writing to the store', () => {
       dataIdFromObject: getIdField,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'a',
         array1: [
@@ -831,7 +833,7 @@ describe('writing to the store', () => {
       dataIdFromObject: getIdField,
     });
 
-    expect(normalized).toEqual({
+    expect(normalized.toObject()).toEqual({
       ROOT_QUERY: {
         id: 'a',
         array1: [
@@ -915,7 +917,7 @@ describe('writing to the store', () => {
       dataIdFromObject: getIdField,
     });
 
-    expect(store2).toEqual({
+    expect(store2.toObject()).toEqual({
       ROOT_QUERY: assign({}, result, result2),
     });
   });
@@ -948,7 +950,7 @@ describe('writing to the store', () => {
       writeQueryToStore({
         query,
         result: cloneDeep(result),
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: assign({}, assign({}, omit(result, 'nestedObj')), {
         nestedObj: null,
@@ -977,7 +979,7 @@ describe('writing to the store', () => {
       writeQueryToStore({
         query,
         result: cloneDeep(result),
-      }),
+      }).toObject(),
     ).toEqual({
       ROOT_QUERY: {
         'people_one({"id":"5"})': {
@@ -1146,11 +1148,12 @@ describe('writing to the store', () => {
             selectionSet: def.selectionSet,
             result: cloneDeep(result),
             context: {
-              store: {},
+              storeFactory: defaultNormalizedCacheFactory,
+              store: defaultNormalizedCacheFactory(),
               variables,
               dataIdFromObject: () => '5',
             },
-          }),
+          }).toObject(),
         ).toEqual({
           '5': {
             'some_mutation({"input":{"id":"5","arr":[1,{"a":"b"}],"obj":{"a":"b"},"num":5.5,"nil":null,"bo":true}})': {
@@ -1195,7 +1198,7 @@ describe('writing to the store', () => {
           lastName: 'Smith',
         },
       };
-      const expStore = {
+      const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
           author: {
             type: 'id',
@@ -1204,13 +1207,13 @@ describe('writing to the store', () => {
           },
         },
         '$ROOT_QUERY.author': data.author,
-      };
+      });
       expect(
         writeQueryToStore({
           result: data,
           query,
-        }),
-      ).toEqual(expStore);
+        }).toObject(),
+      ).toEqual(expStore.toObject());
     });
 
     it('should correctly escape real ids', () => {
@@ -1230,7 +1233,7 @@ describe('writing to the store', () => {
           __typename: 'Author',
         },
       };
-      const expStore = {
+      const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
           author: {
             type: 'id',
@@ -1243,14 +1246,14 @@ describe('writing to the store', () => {
           id: data.author.id,
           __typename: data.author.__typename,
         },
-      };
+      });
       expect(
         writeQueryToStore({
           result: data,
           query,
           dataIdFromObject,
-        }),
-      ).toEqual(expStore);
+        }).toObject(),
+      ).toEqual(expStore.toObject());
     });
 
     it('should correctly escape json blobs', () => {
@@ -1272,7 +1275,7 @@ describe('writing to the store', () => {
           __typename: 'Author',
         },
       };
-      const expStore = {
+      const expStore = defaultNormalizedCacheFactory({
         ROOT_QUERY: {
           author: {
             type: 'id',
@@ -1288,14 +1291,14 @@ describe('writing to the store', () => {
             json: data.author.info,
           },
         },
-      };
+      });
       expect(
         writeQueryToStore({
           result: data,
           query,
           dataIdFromObject,
-        }),
-      ).toEqual(expStore);
+        }).toObject(),
+      ).toEqual(expStore.toObject());
     });
   });
 
@@ -1337,7 +1340,7 @@ describe('writing to the store', () => {
         }
       }
     `;
-    const expStoreWithoutId = {
+    const expStoreWithoutId = defaultNormalizedCacheFactory({
       '$ROOT_QUERY.author': {
         firstName: 'John',
         lastName: 'Smith',
@@ -1349,8 +1352,8 @@ describe('writing to the store', () => {
           generated: true,
         },
       },
-    };
-    const expStoreWithId = {
+    });
+    const expStoreWithId = defaultNormalizedCacheFactory({
       Author__129: {
         firstName: 'John',
         lastName: 'Smith',
@@ -1364,20 +1367,20 @@ describe('writing to the store', () => {
           generated: false,
         },
       },
-    };
+    });
     const storeWithoutId = writeQueryToStore({
       result: dataWithoutId,
       query: queryWithoutId,
       dataIdFromObject,
     });
-    expect(storeWithoutId).toEqual(expStoreWithoutId);
+    expect(storeWithoutId.toObject()).toEqual(expStoreWithoutId.toObject());
     const storeWithId = writeQueryToStore({
       result: dataWithId,
       query: queryWithId,
       store: storeWithoutId,
       dataIdFromObject,
     });
-    expect(storeWithId).toEqual(expStoreWithId);
+    expect(storeWithId.toObject()).toEqual(expStoreWithId.toObject());
   });
 
   it('does not swallow errors other than field errors', () => {
@@ -1422,11 +1425,11 @@ describe('writing to the store', () => {
     const newStore = writeQueryToStore({
       query,
       result: cloneDeep(result),
-      store: assign({}, store) as NormalizedCache,
+      store: defaultNormalizedCacheFactory(store.toObject()),
     });
 
-    Object.keys(store).forEach(field => {
-      expect(store[field]).toEqual(newStore[field]);
+    Object.keys(store.toObject()).forEach(field => {
+      expect(store.get(field)).toEqual(newStore.get(field));
     });
   });
 
@@ -1458,7 +1461,7 @@ describe('writing to the store', () => {
         dataIdFromObject: getIdField,
       });
 
-      expect(newStore['1']).toEqual(result.todos[0]);
+      expect(newStore.get('1')).toEqual(result.todos[0]);
     });
 
     it('should warn when it receives the wrong data with non-union fragments (using an heuristic matcher)', () => {
@@ -1482,7 +1485,7 @@ describe('writing to the store', () => {
           fragmentMatcherFunction,
         });
 
-        expect(newStore['1']).toEqual(result.todos[0]);
+        expect(newStore.get('1')).toEqual(result.todos[0]);
       }, /Missing field description/);
     });
 
@@ -1547,7 +1550,7 @@ describe('writing to the store', () => {
           fragmentMatcherFunction,
         });
 
-        expect(newStore['1']).toEqual(result.todos[0]);
+        expect(newStore.get('1')).toEqual(result.todos[0]);
       }, /Missing field price/);
     });
 
@@ -1573,7 +1576,7 @@ describe('writing to the store', () => {
           fragmentMatcherFunction,
         });
 
-        expect(newStore['1']).toEqual(result.todos[0]);
+        expect(newStore.get('1')).toEqual(result.todos[0]);
       }, /Missing field __typename/);
     });
 
@@ -1589,7 +1592,7 @@ describe('writing to the store', () => {
         dataIdFromObject: getIdField,
       });
 
-      expect(newStore['ROOT_QUERY']).toEqual({ todos: null });
+      expect(newStore.get('ROOT_QUERY')).toEqual({ todos: null });
     });
     it('should not warn if a field is defered', () => {
       let originalWarn = console.warn;
@@ -1613,14 +1616,14 @@ describe('writing to the store', () => {
         fragmentMatcherFunction,
       });
 
-      expect(newStore['ROOT_QUERY']).toEqual({ id: 1 });
+      expect(newStore.get('ROOT_QUERY')).toEqual({ id: 1 });
       expect(console.warn).not.toBeCalled();
       console.warn = originalWarn;
     });
   });
 
   it('throws when trying to write an object without id that was previously queried with id', () => {
-    const store = {
+    const store = defaultNormalizedCacheFactory({
       ROOT_QUERY: assign(
         {},
         {
@@ -1640,7 +1643,7 @@ describe('writing to the store', () => {
           stringField: 'This is a string!',
         },
       ) as StoreObject,
-    } as NormalizedCache;
+    });
 
     expect(() => {
       writeQueryToStore({
@@ -1685,7 +1688,7 @@ describe('writing to the store', () => {
   });
 
   it('properly handles the connection directive', () => {
-    const store: NormalizedCache = {};
+    const store = defaultNormalizedCacheFactory();
 
     writeQueryToStore({
       query: gql`
@@ -1723,7 +1726,7 @@ describe('writing to the store', () => {
       store,
     });
 
-    expect(store).toEqual({
+    expect(store.toObject()).toEqual({
       ROOT_QUERY: {
         abc: [
           {
diff --git a/packages/apollo-client/src/__tests__/client.ts b/packages/apollo-client/src/__tests__/client.ts
--- a/packages/apollo-client/src/__tests__/client.ts
+++ b/packages/apollo-client/src/__tests__/client.ts
@@ -1036,7 +1036,7 @@ describe('client', () => {
       typeCondition: string,
       context: any,
     ): boolean => {
-      const obj = context.store[idValue.id];
+      const obj = context.store.get(idValue.id);
 
       if (!obj) {
         return false;