diff --git a/README.md b/README.md
index de97be31ce72f77e3cb4b81a5ad9700e968355e2..d979af0d4c9d165a1fda96d2db4d602a846d5769 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ dspace-angular
 
 This project is currently in pre-alpha.
 
-You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular).
+You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular).
 
 If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype)
 
diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts
index ee7b101f969dcefefc9ebaaacc5d2bd0c75a0457..90ea2026e3190eaf30af6597898dee3e49a570ff 100644
--- a/e2e/app.e2e-spec.ts
+++ b/e2e/app.e2e-spec.ts
@@ -12,8 +12,8 @@ describe('protractor App', function() {
     expect(page.getPageTitleText()).toEqual('DSpace');
   });
 
-  it('should display title "Hello, World!"', () => {
+  it('should display header "Welcome to DSpace"', () => {
     page.navigateTo();
-    expect(page.getFirstPText()).toEqual('Hello, World!');
+    expect(page.getFirstHeaderText()).toEqual('Welcome to DSpace');
   });
 });
diff --git a/e2e/app.po.ts b/e2e/app.po.ts
index 164c524620b30221f43b22e6f106b5bbc49d4b2f..d8d2acf120ed7efd027ef16dc6900b1d34bc192d 100644
--- a/e2e/app.po.ts
+++ b/e2e/app.po.ts
@@ -12,4 +12,8 @@ export class ProtractorPage {
   getFirstPText() {
     return element(by.xpath('//p[1]')).getText();
   }
-}
\ No newline at end of file
+
+  getFirstHeaderText() {
+    return element(by.xpath('//h1[1]')).getText();
+  }
+}
diff --git a/package.json b/package.json
index 316b12f0b6b0a0b272f9c76e78c9abf7ad440bcc..8dd1110d1a9f1c54690f870b54d1a7e20d71ee48 100644
--- a/package.json
+++ b/package.json
@@ -78,19 +78,21 @@
     "@angular/upgrade": "2.2.3",
     "@angularclass/bootloader": "1.0.1",
     "@angularclass/idle-preload": "1.0.4",
-    "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.24",
+    "@ng-bootstrap/ng-bootstrap": "1.0.0-alpha.15",
     "@ngrx/core": "^1.2.0",
     "@ngrx/effects": "^2.0.0",
     "@ngrx/router-store": "^1.2.5",
     "@ngrx/store": "^2.2.1",
     "@ngrx/store-devtools": "^3.2.2",
+    "@ngx-translate/core": "^6.0.1",
+    "@ngx-translate/http-loader": "^0.0.3",
     "@types/jsonschema": "0.0.5",
     "angular2-express-engine": "2.1.0-rc.1",
     "angular2-platform-node": "2.1.0-rc.1",
     "angular2-universal": "2.1.0-rc.1",
     "angular2-universal-polyfills": "2.1.0-rc.1",
     "body-parser": "1.15.2",
-    "bootstrap": "4.0.0-alpha.6",
+    "bootstrap": "4.0.0-alpha.5",
     "cerialize": "^0.1.13",
     "compression": "1.6.2",
     "express": "4.14.0",
@@ -100,9 +102,8 @@
     "jsonschema": "^1.1.1",
     "methods": "1.1.2",
     "morgan": "1.7.0",
-    "ng2-pagination": "^2.0.0",
-    "ng2-translate": "4.2.0",
     "preboot": "4.5.2",
+    "reflect-metadata": "^0.1.10",
     "rxjs": "5.0.0-beta.12",
     "ts-md5": "^1.2.0",
     "webfontloader": "1.6.27",
@@ -160,7 +161,6 @@
     "protractor": "~4.0.14",
     "protractor-istanbul-plugin": "~2.0.0",
     "raw-loader": "0.5.1",
-    "reflect-metadata": "0.1.8",
     "rimraf": "2.5.4",
     "rollup": "0.37.0",
     "rollup-plugin-commonjs": "6.0.0",
diff --git a/resources/i18n/en.json b/resources/i18n/en.json
index 80a68206eae6ad288d2b9f4c73d72ecb2d80bf12..95358dc446b006e90770b5e36772a73a02522cc6 100644
--- a/resources/i18n/en.json
+++ b/resources/i18n/en.json
@@ -1,16 +1,21 @@
 {
-  "example": {
-    "with": {
-      "data": "{{greeting}}, {{recipient}}!"
-    }
-  },
-
   "footer": {
     "copyright": "copyright © 2002-{{ year }}",
     "link.dspace": "DSpace software",
     "link.duraspace": "DuraSpace"
   },
 
+  "item": {
+    "page": {
+      "author": "Author",
+      "abstract": "Abstract",
+      "date": "Date",
+      "uri": "URI",
+      "files": "Files",
+      "collections": "Collections"
+    }
+  },
+
   "nav": {
     "home": "Home"
   },
@@ -31,5 +36,12 @@
     "link": {
       "home-page": "Take me to the home page"
     }
+  },
+
+  "home": {
+    "top-level-communities": {
+      "head": "Communities in DSpace",
+      "help": "Select a community to browse its collections."
+    }
   }
 }
diff --git a/src/app/app.component.html b/src/app/app.component.html
index a83530c27dc863eb30e2cc6f16c3c37767370506..a227b80ab04bcc10553a4e4c003cbb3681b776ab 100644
--- a/src/app/app.component.html
+++ b/src/app/app.component.html
@@ -4,25 +4,6 @@
 
     <main class="main-content">
       <div class="container-fluid">
-        <p>{{ 'example.with.data' | translate:data }}</p>
-        <p>{{ example }}</p>
-        <h2 [ngClass]="{ 'red': EnvConfig.production, 'green': !EnvConfig.production }">
-          <span *ngIf="!EnvConfig.production">development</span>
-          <span *ngIf="EnvConfig.production">production</span>
-        </h2>
-
-        {{options.id}}
-        <!--ds-pagination [paginationOptions]="options"
-                       [collectionSize]="100"
-                       (pageChange)="options.currentPage = $event"
-                       (pageSizeChange)="options.pageSize = $event">
-
-          <ul>
-            <li *ngFor="let item of collection | paginate: { itemsPerPage: options.pageSize, currentPage: options.currentPage, totalItems: 100 }"> {{item}} </li>
-          </ul>
-
-        </ds-pagination-->
-
         <router-outlet></router-outlet>
       </div>
     </main>
diff --git a/src/app/app.component.scss b/src/app/app.component.scss
index 2a58ae0aa28750ad65333e3e958cb596893f944a..7b86523886b55de3cdc5bef7959bbe57735451d1 100644
--- a/src/app/app.component.scss
+++ b/src/app/app.component.scss
@@ -15,11 +15,3 @@
 .main-content {
   flex: 1 0 auto;
 }
-
-h2.red {
-  color: red;
-}
-
-h2.green {
-  color: green;
-}
diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts
index d56be1a807943502e4e1286d6cdc812aecbe9883..9b4aa66bf64c5a788bb7c00d55979d736f4ef7a5 100644
--- a/src/app/app.component.spec.ts
+++ b/src/app/app.component.spec.ts
@@ -10,7 +10,7 @@ import {
   DebugElement
 } from "@angular/core";
 import { By } from '@angular/platform-browser';
-import { TranslateModule, TranslateLoader } from "ng2-translate";
+import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
 import { Store, StoreModule } from "@ngrx/store";
 
 // Load the implementations that should be tested
@@ -34,8 +34,10 @@ describe('App component', () => {
   beforeEach(async(() => {
     return TestBed.configureTestingModule({
       imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
-        provide: TranslateLoader,
-        useClass: MockTranslateLoader
+        loader: {
+          provide: TranslateLoader,
+          useClass: MockTranslateLoader
+        }
       })],
       declarations: [AppComponent], // declare the test component
       providers: [
@@ -52,8 +54,8 @@ describe('App component', () => {
 
     comp = fixture.componentInstance; // component test instance
 
-    // query for the title <p> by CSS element selector
-    de = fixture.debugElement.query(By.css('p'));
+    // query for the <div class="outer-wrapper"> by CSS element selector
+    de = fixture.debugElement.query(By.css('div.outer-wrapper'));
     el = de.nativeElement;
   });
 
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 856f964283f6f8ccce0b6c0a9330344de378a683..1cf97e763cf4cb019a4773bbb1098acd6960b767 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -3,17 +3,14 @@ import {
   ChangeDetectionStrategy,
   Inject,
   ViewEncapsulation,
-  OnDestroy,
   OnInit, HostListener
 } from "@angular/core";
-import { TranslateService } from "ng2-translate";
+import { TranslateService } from "@ngx-translate/core";
 import { HostWindowState } from "./shared/host-window.reducer";
 import { Store } from "@ngrx/store";
 import { HostWindowResizeAction } from "./shared/host-window.actions";
 
-import { PaginationOptions } from './core/shared/pagination-options.model';
-
-import { GLOBAL_CONFIG, GlobalConfig } from '../config';
+import { EnvConfig, GLOBAL_CONFIG, GlobalConfig } from '../config';
 
 @Component({
   changeDetection: ChangeDetectionStrategy.Default,
@@ -22,16 +19,7 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config';
   templateUrl: './app.component.html',
   styleUrls: ['./app.component.css']
 })
-export class AppComponent implements OnDestroy, OnInit {
-  private translateSubscription: any;
-
-  collection = [];
-  example: string;
-  options: PaginationOptions = new PaginationOptions();
-  data: any = {
-    greeting: 'Hello',
-    recipient: 'World'
-  };
+export class AppComponent implements OnInit {
 
   constructor(
     @Inject(GLOBAL_CONFIG) public EnvConfig: GlobalConfig,
@@ -42,26 +30,12 @@ export class AppComponent implements OnDestroy, OnInit {
     translate.setDefaultLang('en');
     // the lang to use, if the lang isn't available, it will use the current loader to get them
     translate.use('en');
-    for (let i = 1; i <= 100; i++) {
-      this.collection.push(`item ${i}`);
-    }
   }
 
   ngOnInit() {
-    this.translateSubscription = this.translate.get('example.with.data', { greeting: 'Hello', recipient: 'DSpace' }).subscribe((translation: string) => {
-      this.example = translation;
-    });
-    this.onLoad();
-    this.options.id = 'app';
-    //this.options.currentPage = 1;
-    this.options.pageSize = 15;
-    this.options.size = 'sm';
-  }
-
-  ngOnDestroy() {
-    if (this.translateSubscription) {
-      this.translateSubscription.unsubscribe();
-    }
+    const env: string = EnvConfig.production ? "Production" : "Development";
+    const color: string = EnvConfig.production ? "red" : "green";
+    console.info(`Environment: %c${env}`,  `color: ${color}; font-weight: bold;`);
   }
 
   @HostListener('window:resize', ['$event'])
@@ -71,9 +45,4 @@ export class AppComponent implements OnDestroy, OnInit {
     );
   }
 
-  private onLoad() {
-    this.store.dispatch(
-      new HostWindowResizeAction(window.innerWidth, window.innerHeight)
-    );
-  }
 }
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 920195a0f5f7a466ec89cee6b6115164082ab948..42304c865e94a4187e470c33d849d557ba3d7b88 100755
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
 
 import { CoreModule } from './core/core.module';
 import { HomeModule } from './home/home.module';
+import { ItemPageModule } from './item-page/item-page.module';
 
 import { SharedModule } from './shared/shared.module';
 
@@ -10,15 +11,17 @@ import { AppComponent } from './app.component';
 import { HeaderComponent } from './header/header.component';
 import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
 
+
 @NgModule({
   declarations: [
     AppComponent,
     HeaderComponent,
-    PageNotFoundComponent
+    PageNotFoundComponent,
   ],
   imports: [
     SharedModule,
     HomeModule,
+    ItemPageModule,
     CoreModule.forRoot(),
     AppRoutingModule
   ],
diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts
new file mode 100644
index 0000000000000000000000000000000000000000..00cb50663aa8ce8e039a6542a17ec8369a53276d
--- /dev/null
+++ b/src/app/core/cache/builders/build-decorators.ts
@@ -0,0 +1,41 @@
+import "reflect-metadata";
+import { GenericConstructor } from "../../shared/generic-constructor";
+import { CacheableObject } from "../object-cache.reducer";
+import { NormalizedDSOType } from "../models/normalized-dspace-object-type";
+
+const mapsToMetadataKey = Symbol("mapsTo");
+const relationshipKey = Symbol("relationship");
+
+const relationshipMap = new Map();
+
+export const mapsTo = function(value: GenericConstructor<CacheableObject>) {
+  return Reflect.metadata(mapsToMetadataKey, value);
+};
+
+export const getMapsTo = function(target: any) {
+  return Reflect.getOwnMetadata(mapsToMetadataKey, target);
+};
+
+export const relationship = function(value: NormalizedDSOType): any {
+  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
+    if (!target || !propertyKey) {
+      return;
+    }
+
+    let metaDataList : Array<string> = relationshipMap.get(target.constructor) || [];
+    if (metaDataList.indexOf(propertyKey) === -1) {
+      metaDataList.push(propertyKey);
+    }
+    relationshipMap.set(target.constructor, metaDataList);
+
+    return Reflect.metadata(relationshipKey, value).apply(this, arguments);
+  };
+};
+
+export const getResourceType = function(target: any, propertyKey: string) {
+  return Reflect.getMetadata(relationshipKey, target, propertyKey);
+};
+
+export const getRelationships = function(target: any) {
+  return relationshipMap.get(target);
+};
diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f37f0ce4f942def6b27b1d388fbcced25185b874
--- /dev/null
+++ b/src/app/core/cache/builders/remote-data-build.service.ts
@@ -0,0 +1,156 @@
+import { Injectable } from "@angular/core";
+import { CacheableObject } from "../object-cache.reducer";
+import { ObjectCacheService } from "../object-cache.service";
+import { RequestService } from "../../data/request.service";
+import { ResponseCacheService } from "../response-cache.service";
+import { Store } from "@ngrx/store";
+import { CoreState } from "../../core.reducers";
+import { RequestEntry } from "../../data/request.reducer";
+import { hasValue, isNotEmpty } from "../../../shared/empty.util";
+import { ResponseCacheEntry } from "../response-cache.reducer";
+import { ErrorResponse, SuccessResponse } from "../response-cache.models";
+import { Observable } from "rxjs/Observable";
+import { RemoteData } from "../../data/remote-data";
+import { GenericConstructor } from "../../shared/generic-constructor";
+import { getMapsTo, getResourceType, getRelationships } from "./build-decorators";
+import { NormalizedDSOFactory } from "../models/normalized-dspace-object-factory";
+
+@Injectable()
+export class RemoteDataBuildService {
+  constructor(
+    protected objectCache: ObjectCacheService,
+    protected responseCache: ResponseCacheService,
+    protected requestService: RequestService,
+    protected store: Store<CoreState>,
+  ) {
+  }
+
+  buildSingle<TNormalized extends CacheableObject, TDomain>(
+    href: string,
+    normalizedType: GenericConstructor<TNormalized>
+  ): RemoteData<TDomain> {
+    const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href);
+    const responseCacheObs = this.responseCache.get(href);
+
+    const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged();
+
+    const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged();
+
+    const isSuccessFul = responseCacheObs
+      .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged();
+
+    const errorMessage = responseCacheObs
+      .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful)
+      .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
+      .distinctUntilChanged();
+
+    const payload =
+      Observable.race(
+        this.objectCache.getBySelfLink<TNormalized>(href, normalizedType),
+        responseCacheObs
+          .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
+          .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
+          .flatMap((resourceUUIDs: Array<string>) => {
+            if (isNotEmpty(resourceUUIDs)) {
+              return this.objectCache.get(resourceUUIDs[0], normalizedType);
+            }
+            else {
+              return Observable.of(undefined);
+            }
+          })
+          .distinctUntilChanged()
+      ).map((normalized: TNormalized) => {
+        return this.build<TNormalized, TDomain>(normalized);
+      });
+
+    return new RemoteData(
+      href,
+      requestPending,
+      responsePending,
+      isSuccessFul,
+      errorMessage,
+      payload
+    );
+  }
+
+  buildList<TNormalized extends CacheableObject, TDomain>(
+    href: string,
+    normalizedType: GenericConstructor<TNormalized>
+  ): RemoteData<TDomain[]> {
+    const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href);
+    const responseCacheObs = this.responseCache.get(href);
+
+    const requestPending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.requestPending).distinctUntilChanged();
+
+    const responsePending = requestObs.map((entry: RequestEntry) => hasValue(entry) && entry.responsePending).distinctUntilChanged();
+
+    const isSuccessFul = responseCacheObs
+      .map((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful).distinctUntilChanged();
+
+    const errorMessage = responseCacheObs
+      .filter((entry: ResponseCacheEntry) => hasValue(entry) && !entry.response.isSuccessful)
+      .map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
+      .distinctUntilChanged();
+
+    const payload = responseCacheObs
+      .filter((entry: ResponseCacheEntry) => hasValue(entry) && entry.response.isSuccessful)
+      .map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
+      .flatMap((resourceUUIDs: Array<string>) => {
+        return this.objectCache.getList(resourceUUIDs, normalizedType)
+          .map((normList: TNormalized[]) => {
+            return normList.map((normalized: TNormalized) => {
+              return this.build<TNormalized, TDomain>(normalized);
+            });
+          });
+      })
+      .distinctUntilChanged();
+
+    return new RemoteData(
+      href,
+      requestPending,
+      responsePending,
+      isSuccessFul,
+      errorMessage,
+      payload
+    );
+  }
+
+
+  build<TNormalized extends CacheableObject, TDomain>(normalized: TNormalized): TDomain {
+    let links: any = {};
+
+    const relationships = getRelationships(normalized.constructor) || [];
+
+    relationships.forEach((relationship: string) => {
+      if (hasValue(normalized[relationship])) {
+        const resourceType = getResourceType(normalized, relationship);
+        const resourceConstructor = NormalizedDSOFactory.getConstructor(resourceType);
+        if (Array.isArray(normalized[relationship])) {
+          // without the setTimeout, the actions inside requestService.configure
+          // are dispatched, but sometimes don't arrive. I'm unsure why atm.
+          setTimeout(() => {
+            normalized[relationship].forEach((href: string) => {
+              this.requestService.configure(href, resourceConstructor)
+            });
+          }, 0);
+
+          links[relationship] = normalized[relationship].map((href: string) => {
+            return this.buildSingle(href, resourceConstructor);
+          });
+        }
+        else {
+          // without the setTimeout, the actions inside requestService.configure
+          // are dispatched, but sometimes don't arrive. I'm unsure why atm.
+          setTimeout(() => {
+            this.requestService.configure(normalized[relationship], resourceConstructor);
+          },0);
+
+          links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
+        }
+      }
+    });
+
+    const domainModel = getMapsTo(normalized.constructor);
+    return Object.assign(new domainModel(), normalized, links);
+  }
+}
diff --git a/src/app/core/cache/cache.reducers.ts b/src/app/core/cache/cache.reducers.ts
index 2edd1e8ebf87f1e00cd56840c23d30d1794cd50c..b5cd5c7b41f82287a8c232751b9dd48c6d897d40 100644
--- a/src/app/core/cache/cache.reducers.ts
+++ b/src/app/core/cache/cache.reducers.ts
@@ -1,14 +1,14 @@
 import { combineReducers } from "@ngrx/store";
-import { RequestCacheState, requestCacheReducer } from "./request-cache.reducer";
+import { ResponseCacheState, responseCacheReducer } from "./response-cache.reducer";
 import { ObjectCacheState, objectCacheReducer } from "./object-cache.reducer";
 
 export interface CacheState {
-  request: RequestCacheState,
+  response: ResponseCacheState,
   object: ObjectCacheState
 }
 
 export const reducers = {
-  request: requestCacheReducer,
+  response: responseCacheReducer,
   object: objectCacheReducer
 };
 
diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..57b4f6334664ee24c459a07a11437f02686d47c0
--- /dev/null
+++ b/src/app/core/cache/models/normalized-bitstream.model.ts
@@ -0,0 +1,43 @@
+import { inheritSerialization, autoserialize } from "cerialize";
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { Bitstream } from "../../shared/bitstream.model";
+import { mapsTo } from "../builders/build-decorators";
+
+@mapsTo(Bitstream)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedBitstream extends NormalizedDSpaceObject {
+
+    /**
+     * The size of this bitstream in bytes(?)
+     */
+    @autoserialize
+    size: number;
+
+    /**
+     * The relative path to this Bitstream's file
+     */
+    url: string;
+
+    /**
+     * The mime type of this Bitstream
+     */
+    mimetype: string;
+
+    /**
+     * The description of this Bitstream
+     */
+    description: string;
+
+    /**
+     * An array of Bundles that are direct parents of this Bitstream
+     */
+    parents: Array<string>;
+
+    /**
+     * The Bundle that owns this Bitstream
+     */
+    owner: string;
+
+    @autoserialize
+    retrieve: string;
+}
diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bb0e7b07082d721887bccb22ade6b33d9ac77dc7
--- /dev/null
+++ b/src/app/core/cache/models/normalized-bundle.model.ts
@@ -0,0 +1,30 @@
+import { autoserialize, inheritSerialization } from "cerialize";
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { Bundle } from "../../shared/bundle.model";
+import { mapsTo, relationship } from "../builders/build-decorators";
+import { NormalizedDSOType } from "./normalized-dspace-object-type";
+
+@mapsTo(Bundle)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedBundle extends NormalizedDSpaceObject {
+  /**
+   * The primary bitstream of this Bundle
+   */
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedBitstream)
+  primaryBitstream: string;
+
+  /**
+   * An array of Items that are direct parents of this Bundle
+   */
+  parents: Array<string>;
+
+  /**
+   * The Item that owns this Bundle
+   */
+  owner: string;
+
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedBitstream)
+  bitstreams: Array<string>;
+}
diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9b31f34837e0e57780490c831495d4907e8312be
--- /dev/null
+++ b/src/app/core/cache/models/normalized-collection.model.ts
@@ -0,0 +1,36 @@
+import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize";
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { Collection } from "../../shared/collection.model";
+import { mapsTo, relationship } from "../builders/build-decorators";
+import { NormalizedDSOType } from "./normalized-dspace-object-type";
+
+@mapsTo(Collection)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedCollection extends NormalizedDSpaceObject {
+
+  /**
+   * A string representing the unique handle of this Collection
+   */
+  @autoserialize
+  handle: string;
+
+  /**
+   * The Bitstream that represents the logo of this Collection
+   */
+  logo: string;
+
+  /**
+   * An array of Collections that are direct parents of this Collection
+   */
+  parents: Array<string>;
+
+  /**
+   * The Collection that owns this Collection
+   */
+  owner: string;
+
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedItem)
+  items: Array<string>;
+
+}
diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..774abcc979baa819a08cf3352b87f18f2908990e
--- /dev/null
+++ b/src/app/core/cache/models/normalized-community.model.ts
@@ -0,0 +1,36 @@
+import { autoserialize, inheritSerialization, autoserializeAs } from "cerialize";
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { Community } from "../../shared/community.model";
+import { mapsTo, relationship } from "../builders/build-decorators";
+import { NormalizedDSOType } from "./normalized-dspace-object-type";
+
+@mapsTo(Community)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedCommunity extends NormalizedDSpaceObject {
+
+  /**
+   * A string representing the unique handle of this Community
+   */
+  @autoserialize
+  handle: string;
+
+  /**
+   * The Bitstream that represents the logo of this Community
+   */
+  logo: string;
+
+  /**
+   * An array of Communities that are direct parents of this Community
+   */
+  parents: Array<string>;
+
+  /**
+   * The Community that owns this Community
+   */
+  owner: string;
+
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedCollection)
+  collections: Array<string>;
+
+}
diff --git a/src/app/core/cache/models/normalized-dspace-object-factory.ts b/src/app/core/cache/models/normalized-dspace-object-factory.ts
new file mode 100644
index 0000000000000000000000000000000000000000..052f7be3ee9bbf1cb9ee2bc6092736ccb757adfd
--- /dev/null
+++ b/src/app/core/cache/models/normalized-dspace-object-factory.ts
@@ -0,0 +1,33 @@
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { NormalizedBitstream } from "./normalized-bitstream.model";
+import { NormalizedBundle } from "./normalized-bundle.model";
+import { NormalizedItem } from "./normalized-item.model";
+import { NormalizedCollection } from "./normalized-collection.model";
+import { GenericConstructor } from "../../shared/generic-constructor";
+import { NormalizedDSOType } from "./normalized-dspace-object-type";
+import { NormalizedCommunity } from "./normalized-community.model";
+
+export class NormalizedDSOFactory {
+  public static getConstructor(type: NormalizedDSOType): GenericConstructor<NormalizedDSpaceObject> {
+    switch (type) {
+      case NormalizedDSOType.NormalizedBitstream: {
+        return NormalizedBitstream
+      }
+      case NormalizedDSOType.NormalizedBundle: {
+        return NormalizedBundle
+      }
+      case NormalizedDSOType.NormalizedItem: {
+        return NormalizedItem
+      }
+      case NormalizedDSOType.NormalizedCollection: {
+        return NormalizedCollection
+      }
+      case NormalizedDSOType.NormalizedCommunity: {
+        return NormalizedCommunity
+      }
+      default: {
+        return undefined;
+      }
+    }
+  }
+}
diff --git a/src/app/core/cache/models/normalized-dspace-object-type.ts b/src/app/core/cache/models/normalized-dspace-object-type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8ac9215b441094d3ec1fbe0010a886fb2ba3e102
--- /dev/null
+++ b/src/app/core/cache/models/normalized-dspace-object-type.ts
@@ -0,0 +1,7 @@
+export enum NormalizedDSOType {
+  NormalizedBitstream,
+  NormalizedBundle,
+  NormalizedItem,
+  NormalizedCollection,
+  NormalizedCommunity
+}
diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..29688d5f9dff4854cad7549c973665f212165dbd
--- /dev/null
+++ b/src/app/core/cache/models/normalized-dspace-object.model.ts
@@ -0,0 +1,52 @@
+import { autoserialize, autoserializeAs } from "cerialize";
+import { CacheableObject } from "../object-cache.reducer";
+import { Metadatum } from "../../shared/metadatum.model";
+
+/**
+ * An abstract model class for a DSpaceObject.
+ */
+export abstract class NormalizedDSpaceObject implements CacheableObject {
+
+  @autoserialize
+  self: string;
+
+  /**
+   * The human-readable identifier of this DSpaceObject
+   */
+  @autoserialize
+  id: string;
+
+  /**
+   * The universally unique identifier of this DSpaceObject
+   */
+  @autoserialize
+  uuid: string;
+
+  /**
+   * A string representing the kind of DSpaceObject, e.g. community, item, …
+   */
+  type: string;
+
+  /**
+   * The name for this DSpaceObject
+   */
+  @autoserialize
+  name: string;
+
+  /**
+   * An array containing all metadata of this DSpaceObject
+   */
+  @autoserializeAs(Metadatum)
+  metadata: Array<Metadatum>;
+
+  /**
+   * An array of DSpaceObjects that are direct parents of this DSpaceObject
+   */
+  @autoserialize
+  parents: Array<string>;
+
+  /**
+   * The DSpaceObject that owns this DSpaceObject
+   */
+  owner: string;
+}
diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..cdd3acdb923eccc5bdda574dded3c192b9b95ebb
--- /dev/null
+++ b/src/app/core/cache/models/normalized-item.model.ts
@@ -0,0 +1,47 @@
+import { inheritSerialization, autoserialize } from "cerialize";
+import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
+import { Item } from "../../shared/item.model";
+import { mapsTo, relationship } from "../builders/build-decorators";
+import { NormalizedDSOType } from "./normalized-dspace-object-type";
+
+@mapsTo(Item)
+@inheritSerialization(NormalizedDSpaceObject)
+export class NormalizedItem extends NormalizedDSpaceObject {
+
+  /**
+   * A string representing the unique handle of this Item
+   */
+  @autoserialize
+  handle: string;
+
+  /**
+   * The Date of the last modification of this Item
+   */
+  lastModified: Date;
+
+  /**
+   * A boolean representing if this Item is currently archived or not
+   */
+  isArchived: boolean;
+
+  /**
+   * A boolean representing if this Item is currently withdrawn or not
+   */
+  isWithdrawn: boolean;
+
+  /**
+   * An array of Collections that are direct parents of this Item
+   */
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedCollection)
+  parents: Array<string>;
+
+  /**
+   * The Collection that owns this Item
+   */
+  owner: string;
+
+  @autoserialize
+  @relationship(NormalizedDSOType.NormalizedBundle)
+  bundles: Array<string>;
+}
diff --git a/src/app/core/shared/pagination-options.model.ts b/src/app/core/cache/models/pagination-options.model.ts
similarity index 100%
rename from src/app/core/shared/pagination-options.model.ts
rename to src/app/core/cache/models/pagination-options.model.ts
diff --git a/src/app/core/cache/models/self-link.model.ts b/src/app/core/cache/models/self-link.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5adc78062a747984104149e6313e2eb41885ab7b
--- /dev/null
+++ b/src/app/core/cache/models/self-link.model.ts
@@ -0,0 +1,10 @@
+import { autoserialize } from "cerialize";
+
+export class SelfLink {
+
+  @autoserialize
+  self: string;
+
+  @autoserialize
+  uuid: string;
+}
diff --git a/src/app/core/shared/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts
similarity index 100%
rename from src/app/core/shared/sort-options.model.ts
rename to src/app/core/cache/models/sort-options.model.ts
diff --git a/src/app/core/cache/object-cache.reducer.ts b/src/app/core/cache/object-cache.reducer.ts
index 23b01882161aedcec48eb6055d7b4ba0b38ed817..85e1fdc2b303586befcf5fb7ad8cc2e1dc074e8d 100644
--- a/src/app/core/cache/object-cache.reducer.ts
+++ b/src/app/core/cache/object-cache.reducer.ts
@@ -12,6 +12,7 @@ import { CacheEntry } from "./cache-entry";
  */
 export interface CacheableObject {
   uuid: string;
+  self?: string;
 }
 
 /**
diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts
index 9093093f50da4717ef78100fc896b2d82091a92a..ec0bea4a97b8afe3fa14aef059b2bb0fff12fcd6 100644
--- a/src/app/core/cache/object-cache.service.ts
+++ b/src/app/core/cache/object-cache.service.ts
@@ -60,6 +60,11 @@ export class ObjectCacheService {
       .map((entry: ObjectCacheEntry) => <T> Object.assign(new type(), entry.data));
   }
 
+  getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> {
+    return this.store.select<string>('core', 'index', 'href', href)
+      .flatMap((uuid: string) => this.get(uuid, type))
+  }
+
   /**
    * Get an observable for an array of objects of the same type
    * with the specified UUIDs
@@ -104,6 +109,25 @@ export class ObjectCacheService {
     return result;
   }
 
+  /**
+   * Check whether the object with the specified self link is cached
+   *
+   * @param href
+   *    The self link of the object to check
+   * @return boolean
+   *    true if the object with the specified self link is cached,
+   *    false otherwise
+   */
+  hasBySelfLink(href: string): boolean {
+    let result: boolean = false;
+
+    this.store.select<string>('core', 'index', 'href', href)
+      .take(1)
+      .subscribe((uuid: string) => result = this.has(uuid));
+
+    return result;
+  }
+
   /**
    * Check whether an ObjectCacheEntry should still be cached
    *
diff --git a/src/app/core/cache/request-cache.actions.ts b/src/app/core/cache/request-cache.actions.ts
deleted file mode 100644
index 78c6692d71a2e19d6315ded4b82213f16bd2a82e..0000000000000000000000000000000000000000
--- a/src/app/core/cache/request-cache.actions.ts
+++ /dev/null
@@ -1,205 +0,0 @@
-import { OpaqueToken } from "@angular/core";
-import { Action } from "@ngrx/store";
-import { type } from "../../shared/ngrx/type";
-import { PaginationOptions } from "../shared/pagination-options.model";
-import { SortOptions } from "../shared/sort-options.model";
-
-/**
- * The list of RequestCacheAction type definitions
- */
-export const RequestCacheActionTypes = {
-  FIND_BY_ID: type('dspace/core/cache/request/FIND_BY_ID'),
-  FIND_ALL: type('dspace/core/cache/request/FIND_ALL'),
-  SUCCESS: type('dspace/core/cache/request/SUCCESS'),
-  ERROR: type('dspace/core/cache/request/ERROR'),
-  REMOVE: type('dspace/core/cache/request/REMOVE'),
-  RESET_TIMESTAMPS: type('dspace/core/cache/request/RESET_TIMESTAMPS')
-};
-
-/**
- * An ngrx action to find all objects of a certain type
- */
-export class RequestCacheFindAllAction implements Action {
-  type = RequestCacheActionTypes.FIND_ALL;
-  payload: {
-    key: string,
-    service: OpaqueToken,
-    scopeID: string,
-    paginationOptions: PaginationOptions,
-    sortOptions: SortOptions
-  };
-
-  /**
-   * Create a new RequestCacheFindAllAction
-   *
-   * @param key
-   *    the key under which to cache this request, should be unique
-   * @param service
-   *    the name of the service that initiated the action
-   * @param scopeID
-   *    the id of an optional scope object
-   * @param paginationOptions
-   *    the pagination options
-   * @param sortOptions
-   *    the sort options
-   */
-  constructor(
-    key: string,
-    service: OpaqueToken,
-    scopeID?: string,
-    paginationOptions: PaginationOptions = new PaginationOptions(),
-    sortOptions: SortOptions = new SortOptions()
-  ) {
-    this.payload = {
-      key,
-      service,
-      scopeID,
-      paginationOptions,
-      sortOptions
-    }
-  }
-}
-
-/**
- * An ngrx action to find objects by id
- */
-export class RequestCacheFindByIDAction implements Action {
-  type = RequestCacheActionTypes.FIND_BY_ID;
-  payload: {
-    key: string,
-    service: OpaqueToken,
-    resourceID: string
-  };
-
-  /**
-   * Create a new RequestCacheFindByIDAction
-   *
-   * @param key
-   *    the key under which to cache this request, should be unique
-   * @param service
-   *    the name of the service that initiated the action
-   * @param resourceID
-   *    the ID of the resource to find
-   */
-  constructor(
-    key: string,
-    service: OpaqueToken,
-    resourceID: string
-  ) {
-    this.payload = {
-      key,
-      service,
-      resourceID
-    }
-  }
-}
-
-/**
- * An ngrx action to indicate a request was returned successful
- */
-export class RequestCacheSuccessAction implements Action {
-  type = RequestCacheActionTypes.SUCCESS;
-  payload: {
-    key: string,
-    resourceUUIDs: Array<string>,
-    timeAdded: number,
-    msToLive: number
-  };
-
-  /**
-   * Create a new RequestCacheSuccessAction
-   *
-   * @param key
-   *    the key under which cache this request is cached,
-   *    should be identical to the one used in the corresponding
-   *    find action
-   * @param resourceUUIDs
-   *    the UUIDs returned from the backend
-   * @param timeAdded
-   *    the time it was returned
-   * @param msToLive
-   *    the amount of milliseconds before it should expire
-   */
-  constructor(key: string, resourceUUIDs: Array<string>, timeAdded, msToLive: number) {
-    this.payload = {
-      key,
-      resourceUUIDs,
-      timeAdded,
-      msToLive
-    };
-  }
-}
-
-/**
- * An ngrx action to indicate a request failed
- */
-export class RequestCacheErrorAction implements Action {
-  type = RequestCacheActionTypes.ERROR;
-  payload: {
-    key: string,
-    errorMessage: string
-  };
-
-  /**
-   * Create a new RequestCacheErrorAction
-   *
-   * @param key
-   *    the key under which cache this request is cached,
-   *    should be identical to the one used in the corresponding
-   *    find action
-   * @param errorMessage
-   *    A message describing the reason the request failed
-   */
-  constructor(key: string, errorMessage: string) {
-    this.payload = {
-      key,
-      errorMessage
-    };
-  }
-}
-
-/**
- * An ngrx action to remove a request from the cache
- */
-export class RequestCacheRemoveAction implements Action {
-  type = RequestCacheActionTypes.REMOVE;
-  payload: string;
-
-  /**
-   * Create a new RequestCacheRemoveAction
-   * @param key
-   *    The key of the request to remove
-   */
-  constructor(key: string) {
-    this.payload = key;
-  }
-}
-
-/**
- * An ngrx action to reset the timeAdded property of all cached objects
- */
-export class ResetRequestCacheTimestampsAction implements Action {
-  type = RequestCacheActionTypes.RESET_TIMESTAMPS;
-  payload: number;
-
-  /**
-   * Create a new ResetObjectCacheTimestampsAction
-   *
-   * @param newTimestamp
-   *    the new timeAdded all objects should get
-   */
-  constructor(newTimestamp: number) {
-    this.payload = newTimestamp;
-  }
-}
-
-/**
- * A type to encompass all RequestCacheActions
- */
-export type RequestCacheAction
-  = RequestCacheFindAllAction
-  | RequestCacheFindByIDAction
-  | RequestCacheSuccessAction
-  | RequestCacheErrorAction
-  | RequestCacheRemoveAction
-  | ResetRequestCacheTimestampsAction;
diff --git a/src/app/core/cache/request-cache.reducer.spec.ts b/src/app/core/cache/request-cache.reducer.spec.ts
deleted file mode 100644
index a478f0c2f5c4914673108b30ad25178e0a344e33..0000000000000000000000000000000000000000
--- a/src/app/core/cache/request-cache.reducer.spec.ts
+++ /dev/null
@@ -1,240 +0,0 @@
-import { requestCacheReducer, RequestCacheState } from "./request-cache.reducer";
-import {
-  RequestCacheRemoveAction, RequestCacheFindByIDAction,
-  RequestCacheFindAllAction, RequestCacheSuccessAction, RequestCacheErrorAction,
-  ResetRequestCacheTimestampsAction
-} from "./request-cache.actions";
-import deepFreeze = require("deep-freeze");
-import { OpaqueToken } from "@angular/core";
-import { PaginationOptions } from "../shared/pagination-options.model";
-
-class NullAction extends RequestCacheRemoveAction {
-  type = null;
-  payload = null;
-
-  constructor() {
-    super(null);
-  }
-}
-
-describe("requestCacheReducer", () => {
-  const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
-  const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
-  const msToLive = 900000;
-  const uuids = [
-    "9e32a2e2-6b91-4236-a361-995ccdc14c60",
-    "598ce822-c357-46f3-ab70-63724d02d6ad",
-    "be8325f7-243b-49f4-8a4b-df2b793ff3b5"
-  ];
-  const resourceID = "9978";
-  const paginationOptions: PaginationOptions = {
-    "id": "test",
-    "currentPage": 1,
-    "pageSizeOptions": [5, 10, 20, 40, 60, 80, 100],
-    "disabled": false,
-    "boundaryLinks": false,
-    "directionLinks": true,
-    "ellipses": true,
-    "maxSize": 0,
-    "pageSize": 10,
-    "rotate": false,
-    "size": 'sm'
-  };
-  const sortOptions = { "field": "id", "direction": 0 };
-  const testState = {
-    [keys[0]]: {
-      "key": keys[0],
-      "service": services[0],
-      "resourceUUIDs": [uuids[0], uuids[1]],
-      "isLoading": false,
-      "paginationOptions": paginationOptions,
-      "sortOptions": sortOptions,
-      "timeAdded": new Date().getTime(),
-      "msToLive": msToLive
-    },
-    [keys[1]]: {
-      "key": keys[1],
-      "service": services[1],
-      "resourceID": resourceID,
-      "resourceUUIDs": [uuids[2]],
-      "isLoading": false,
-      "timeAdded": new Date().getTime(),
-      "msToLive": msToLive
-    }
-  };
-  deepFreeze(testState);
-  const errorState: {} = {
-    [keys[0]]: {
-      errorMessage: 'error',
-      resourceUUIDs: uuids
-    }
-  };
-  deepFreeze(errorState);
-
-
-  it("should return the current state when no valid actions have been made", () => {
-    const action = new NullAction();
-    const newState = requestCacheReducer(testState, action);
-
-    expect(newState).toEqual(testState);
-  });
-
-  it("should start with an empty cache", () => {
-    const action = new NullAction();
-    const initialState = requestCacheReducer(undefined, action);
-
-    expect(initialState).toEqual(Object.create(null));
-  });
-
-  describe("FIND_BY_ID", () => {
-    const action = new RequestCacheFindByIDAction(keys[0], services[0], resourceID);
-
-    it("should perform the action without affecting the previous state", () => {
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should add the request to the cache", () => {
-      const state = Object.create(null);
-      const newState = requestCacheReducer(state, action);
-      expect(newState[keys[0]].key).toBe(keys[0]);
-      expect(newState[keys[0]].service).toEqual(services[0]);
-      expect(newState[keys[0]].resourceID).toBe(resourceID);
-    });
-
-    it("should set isLoading to true", () => {
-      const state = Object.create(null);
-      const newState = requestCacheReducer(state, action);
-      expect(newState[keys[0]].isLoading).toBe(true);
-    });
-
-    it("should remove any previous error message or resourceUUID for the request", () => {
-      const newState = requestCacheReducer(errorState, action);
-      expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
-      expect(newState[keys[0]].errorMessage).toBeUndefined();
-    });
-  });
-
-  describe("FIND_ALL", () => {
-    const action = new RequestCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
-
-    it("should perform the action without affecting the previous state", () => {
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should add the request to the cache", () => {
-      const state = Object.create(null);
-      const newState = requestCacheReducer(state, action);
-      expect(newState[keys[0]].key).toBe(keys[0]);
-      expect(newState[keys[0]].service).toEqual(services[0]);
-      expect(newState[keys[0]].scopeID).toBe(resourceID);
-      expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
-      expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
-    });
-
-    it("should set isLoading to true", () => {
-      const state = Object.create(null);
-      const newState = requestCacheReducer(state, action);
-      expect(newState[keys[0]].isLoading).toBe(true);
-    });
-
-    it("should remove any previous error message or resourceUUIDs for the request", () => {
-      const newState = requestCacheReducer(errorState, action);
-      expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
-      expect(newState[keys[0]].errorMessage).toBeUndefined();
-    });
-  });
-
-  describe("SUCCESS", () => {
-    const successUUIDs = [uuids[0], uuids[2]];
-    const successTimeAdded = new Date().getTime();
-    const successMsToLive = 5;
-    const action = new RequestCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
-
-    it("should perform the action without affecting the previous state", () => {
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should add the response to the cached request", () => {
-      const newState = requestCacheReducer(testState, action);
-      expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
-      expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
-      expect(newState[keys[0]].msToLive).toBe(successMsToLive);
-    });
-
-    it("should set isLoading to false", () => {
-      const newState = requestCacheReducer(testState, action);
-      expect(newState[keys[0]].isLoading).toBe(false);
-    });
-
-    it("should remove any previous error message for the request", () => {
-      const newState = requestCacheReducer(errorState, action);
-      expect(newState[keys[0]].errorMessage).toBeUndefined();
-    });
-  });
-
-  describe("ERROR", () => {
-    const errorMsg = 'errorMsg';
-    const action = new RequestCacheErrorAction(keys[0], errorMsg);
-
-    it("should perform the action without affecting the previous state", () => {
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should set an error message for the request", () => {
-      const newState = requestCacheReducer(errorState, action);
-      expect(newState[keys[0]].errorMessage).toBe(errorMsg);
-    });
-
-    it("should set isLoading to false", () => {
-      const newState = requestCacheReducer(testState, action);
-      expect(newState[keys[0]].isLoading).toBe(false);
-    });
-  });
-
-  describe("REMOVE", () => {
-    it("should perform the action without affecting the previous state", () => {
-      const action = new RequestCacheRemoveAction(keys[0]);
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should remove the specified request from the cache", () => {
-      const action = new RequestCacheRemoveAction(keys[0]);
-      const newState = requestCacheReducer(testState, action);
-      expect(testState[keys[0]]).not.toBeUndefined();
-      expect(newState[keys[0]]).toBeUndefined();
-    });
-
-    it("shouldn't do anything when the specified key isn't cached", () => {
-      const wrongKey = "this isn't cached";
-      const action = new RequestCacheRemoveAction(wrongKey);
-      const newState = requestCacheReducer(testState, action);
-      expect(testState[wrongKey]).toBeUndefined();
-      expect(newState).toEqual(testState);
-    });
-  });
-
-  describe("RESET_TIMESTAMPS", () => {
-    const newTimeStamp = new Date().getTime();
-    const action = new ResetRequestCacheTimestampsAction(newTimeStamp);
-
-    it("should perform the action without affecting the previous state", () => {
-      //testState has already been frozen above
-      requestCacheReducer(testState, action);
-    });
-
-    it("should set the timestamp of all requests in the cache", () => {
-      const newState = requestCacheReducer(testState, action);
-      Object.keys(newState).forEach((key) => {
-        expect(newState[key].timeAdded).toEqual(newTimeStamp);
-      });
-    });
-
-  });
-
-
-});
diff --git a/src/app/core/cache/request-cache.reducer.ts b/src/app/core/cache/request-cache.reducer.ts
deleted file mode 100644
index 0aa2e8c920df13eed975a3bd9a47bddc30305393..0000000000000000000000000000000000000000
--- a/src/app/core/cache/request-cache.reducer.ts
+++ /dev/null
@@ -1,212 +0,0 @@
-import { PaginationOptions } from "../shared/pagination-options.model";
-import { SortOptions } from "../shared/sort-options.model";
-import {
-  RequestCacheAction, RequestCacheActionTypes, RequestCacheFindAllAction,
-  RequestCacheSuccessAction, RequestCacheErrorAction, RequestCacheFindByIDAction,
-  RequestCacheRemoveAction, ResetRequestCacheTimestampsAction
-} from "./request-cache.actions";
-import { OpaqueToken } from "@angular/core";
-import { CacheEntry } from "./cache-entry";
-import { hasValue } from "../../shared/empty.util";
-
-/**
- * An entry in the RequestCache
- */
-export class RequestCacheEntry implements CacheEntry {
-  service: OpaqueToken;
-  key: string;
-  scopeID: string;
-  resourceID: string;
-  resourceUUIDs: Array<String>;
-  resourceType: String;
-  isLoading: boolean;
-  errorMessage: string;
-  paginationOptions: PaginationOptions;
-  sortOptions: SortOptions;
-  timeAdded: number;
-  msToLive: number;
-}
-
-/**
- * The RequestCache State
- */
-export interface RequestCacheState {
-  [key: string]: RequestCacheEntry
-}
-
-// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
-const initialState = Object.create(null);
-
-/**
- * The RequestCache Reducer
- *
- * @param state
- *    the current state
- * @param action
- *    the action to perform on the state
- * @return RequestCacheState
- *    the new state
- */
-export const requestCacheReducer = (state = initialState, action: RequestCacheAction): RequestCacheState => {
-  switch (action.type) {
-
-    case RequestCacheActionTypes.FIND_ALL: {
-      return findAllRequest(state, <RequestCacheFindAllAction> action);
-    }
-
-    case RequestCacheActionTypes.FIND_BY_ID: {
-      return findByIDRequest(state, <RequestCacheFindByIDAction> action);
-    }
-
-    case RequestCacheActionTypes.SUCCESS: {
-      return success(state, <RequestCacheSuccessAction> action);
-    }
-
-    case RequestCacheActionTypes.ERROR: {
-      return error(state, <RequestCacheErrorAction> action);
-    }
-
-    case RequestCacheActionTypes.REMOVE: {
-      return removeFromCache(state, <RequestCacheRemoveAction> action);
-    }
-
-    case RequestCacheActionTypes.RESET_TIMESTAMPS: {
-      return resetRequestCacheTimestamps(state, <ResetRequestCacheTimestampsAction>action)
-    }
-
-    default: {
-      return state;
-    }
-  }
-};
-
-/**
- * Add a FindAll request to the cache
- *
- * @param state
- *    the current state
- * @param action
- *    a RequestCacheFindAllAction
- * @return RequestCacheState
- *    the new state, with the request added, or overwritten
- */
-function findAllRequest(state: RequestCacheState, action: RequestCacheFindAllAction): RequestCacheState {
-  return Object.assign({}, state, {
-    [action.payload.key]: {
-      key: action.payload.key,
-      service: action.payload.service,
-      scopeID: action.payload.scopeID,
-      resourceUUIDs: [],
-      isLoading: true,
-      errorMessage: undefined,
-      paginationOptions: action.payload.paginationOptions,
-      sortOptions: action.payload.sortOptions
-    }
-  });
-}
-
-/**
- * Add a FindByID request to the cache
- *
- * @param state
- *    the current state
- * @param action
- *    a RequestCacheFindByIDAction
- * @return RequestCacheState
- *    the new state, with the request added, or overwritten
- */
-function findByIDRequest(state: RequestCacheState, action: RequestCacheFindByIDAction): RequestCacheState {
-  return Object.assign({}, state, {
-    [action.payload.key]: {
-      key: action.payload.key,
-      service: action.payload.service,
-      resourceID: action.payload.resourceID,
-      resourceUUIDs: [],
-      isLoading: true,
-      errorMessage: undefined,
-    }
-  });
-}
-
-/**
- * Update a cached request with a successful response
- *
- * @param state
- *    the current state
- * @param action
- *    a RequestCacheSuccessAction
- * @return RequestCacheState
- *    the new state, with the response added to the request
- */
-function success(state: RequestCacheState, action: RequestCacheSuccessAction): RequestCacheState {
-  return Object.assign({}, state, {
-    [action.payload.key]: Object.assign({}, state[action.payload.key], {
-      isLoading: false,
-      resourceUUIDs: action.payload.resourceUUIDs,
-      errorMessage: undefined,
-      timeAdded: action.payload.timeAdded,
-      msToLive: action.payload.msToLive
-    })
-  });
-}
-
-/**
- * Update a cached request with an error
- *
- * @param state
- *    the current state
- * @param action
- *    a RequestCacheSuccessAction
- * @return RequestCacheState
- *    the new state, with the error added to the request
- */
-function error(state: RequestCacheState, action: RequestCacheErrorAction): RequestCacheState {
-  return Object.assign({}, state, {
-    [action.payload.key]: Object.assign({}, state[action.payload.key], {
-      isLoading: false,
-      errorMessage: action.payload.errorMessage
-    })
-  });
-}
-
-/**
- * Remove a request from the cache
- *
- * @param state
- *    the current state
- * @param action
- *    an RequestCacheRemoveAction
- * @return RequestCacheState
- *    the new state, with the request removed if it existed.
- */
-function removeFromCache(state: RequestCacheState, action: RequestCacheRemoveAction): RequestCacheState {
-  if (hasValue(state[action.payload])) {
-    let newCache = Object.assign({}, state);
-    delete newCache[action.payload];
-
-    return newCache;
-  }
-  else {
-    return state;
-  }
-}
-
-/**
- * Set the timeAdded timestamp of every cached request to the specified value
- *
- * @param state
- *    the current state
- * @param action
- *    a ResetRequestCacheTimestampsAction
- * @return RequestCacheState
- *    the new state, with all timeAdded timestamps set to the specified value
- */
-function resetRequestCacheTimestamps(state: RequestCacheState, action: ResetRequestCacheTimestampsAction): RequestCacheState {
-  let newState = Object.create(null);
-  Object.keys(state).forEach(key => {
-    newState[key] = Object.assign({}, state[key], {
-      timeAdded: action.payload
-    });
-  });
-  return newState;
-}
diff --git a/src/app/core/cache/request-cache.service.spec.ts b/src/app/core/cache/request-cache.service.spec.ts
deleted file mode 100644
index c29addd23f2eef52d790a5448e897ca6b5853e25..0000000000000000000000000000000000000000
--- a/src/app/core/cache/request-cache.service.spec.ts
+++ /dev/null
@@ -1,160 +0,0 @@
-import { RequestCacheService } from "./request-cache.service";
-import { Store } from "@ngrx/store";
-import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
-import { OpaqueToken } from "@angular/core";
-import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "./request-cache.actions";
-import { Observable } from "rxjs";
-import { PaginationOptions } from "../shared/pagination-options.model";
-
-describe("RequestCacheService", () => {
-  let service: RequestCacheService;
-  let store: Store<RequestCacheState>;
-
-  const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
-  const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
-  const resourceID = "9978";
-  const paginationOptions: PaginationOptions = {
-    "id": "test",
-    "currentPage": 1,
-    "pageSizeOptions": [5, 10, 20, 40, 60, 80, 100],
-    "disabled": false,
-    "boundaryLinks": false,
-    "directionLinks": true,
-    "ellipses": true,
-    "maxSize": 0,
-    "pageSize": 10,
-    "rotate": false,
-    "size": 'sm'
-  };
-  const sortOptions = { "field": "id", "direction": 0 };
-  const timestamp = new Date().getTime();
-  const validCacheEntry = (key) => {
-    return {
-      key: key,
-      timeAdded: timestamp,
-      msToLive: 24 * 60 * 60 * 1000 // a day
-    }
-  };
-  const invalidCacheEntry = (key) => {
-    return {
-      key: key,
-      timeAdded: 0,
-      msToLive: 0
-    }
-  };
-
-  beforeEach(() => {
-    store = new Store<RequestCacheState>(undefined, undefined, undefined);
-    spyOn(store, 'dispatch');
-    service = new RequestCacheService(store);
-    spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
-  });
-
-  describe("findAll", () => {
-    beforeEach(() => {
-      spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
-    });
-    describe("if the key isn't cached", () => {
-      beforeEach(() => {
-          spyOn(service, "has").and.returnValue(false);
-      });
-      it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
-        service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
-        expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
-      });
-      it("should return an observable of the newly cached request with the specified key", () => {
-        let result: RequestCacheEntry;
-        service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
-        expect(result.key).toEqual(keys[0]);
-      });
-    });
-    describe("if the key is already cached", () => {
-      beforeEach(() => {
-          spyOn(service, "has").and.returnValue(true);
-      });
-      it("shouldn't dispatch anything", () => {
-        service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
-        expect(store.dispatch).not.toHaveBeenCalled();
-      });
-      it("should return an observable of the existing cached request with the specified key", () => {
-        let result: RequestCacheEntry;
-        service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
-        expect(result.key).toEqual(keys[0]);
-      });
-    });
-  });
-
-  describe("findById", () => {
-    beforeEach(() => {
-      spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
-    });
-    describe("if the key isn't cached", () => {
-      beforeEach(() => {
-          spyOn(service, "has").and.returnValue(false);
-      });
-      it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
-        service.findById(keys[0], serviceTokens[0], resourceID);
-        expect(store.dispatch).toHaveBeenCalledWith(new RequestCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
-      });
-      it("should return an observable of the newly cached request with the specified key", () => {
-        let result: RequestCacheEntry;
-        service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
-        expect(result.key).toEqual(keys[0]);
-      });
-    });
-    describe("if the key is already cached", () => {
-      beforeEach(() => {
-          spyOn(service, "has").and.returnValue(true);
-      });
-      it("shouldn't dispatch anything", () => {
-        service.findById(keys[0], serviceTokens[0], resourceID);
-        expect(store.dispatch).not.toHaveBeenCalled();
-      });
-      it("should return an observable of the existing cached request with the specified key", () => {
-        let result: RequestCacheEntry;
-        service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
-        expect(result.key).toEqual(keys[0]);
-      });
-    });
-  });
-
-  describe("get", () => {
-    it("should return an observable of the cached request with the specified key", () => {
-      spyOn(store, "select").and.callFake((...args:Array<any>) => {
-        return Observable.of(validCacheEntry(args[args.length - 1]));
-      });
-
-      let testObj: RequestCacheEntry;
-      service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
-      expect(testObj.key).toEqual(keys[1]);
-    });
-
-    it("should not return a cached request that has exceeded its time to live", () => {
-      spyOn(store, "select").and.callFake((...args:Array<any>) => {
-        return Observable.of(invalidCacheEntry(args[args.length - 1]));
-      });
-
-      let getObsHasFired = false;
-      const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
-      expect(getObsHasFired).toBe(false);
-      subscription.unsubscribe();
-    });
-  });
-
-  describe("has", () => {
-    it("should return true if the request with the supplied key is cached and still valid", () => {
-      spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
-      expect(service.has(keys[1])).toBe(true);
-    });
-
-    it("should return false if the request with the supplied key isn't cached", () => {
-      spyOn(store, 'select').and.returnValue(Observable.of(undefined));
-      expect(service.has(keys[1])).toBe(false);
-    });
-
-    it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
-      spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
-      expect(service.has(keys[1])).toBe(false);
-    });
-  });
-});
diff --git a/src/app/core/cache/request-cache.service.ts b/src/app/core/cache/request-cache.service.ts
deleted file mode 100644
index efa7b0d426fc564b7fa34dac2a3b37c875efd4e1..0000000000000000000000000000000000000000
--- a/src/app/core/cache/request-cache.service.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { Injectable, OpaqueToken } from "@angular/core";
-import { Store } from "@ngrx/store";
-import { RequestCacheState, RequestCacheEntry } from "./request-cache.reducer";
-import { Observable } from "rxjs";
-import { hasNoValue } from "../../shared/empty.util";
-import {
-  RequestCacheRemoveAction, RequestCacheFindAllAction,
-  RequestCacheFindByIDAction
-} from "./request-cache.actions";
-import { SortOptions } from "../shared/sort-options.model";
-import { PaginationOptions } from "../shared/pagination-options.model";
-
-/**
- * A service to interact with the request cache
- */
-@Injectable()
-export class RequestCacheService {
-  constructor(
-    private store: Store<RequestCacheState>
-  ) {}
-
-  /**
-   * Start a new findAll request
-   *
-   * This will send a new findAll request to the backend,
-   * and store the request parameters and the fact that
-   * the request is pending
-   *
-   * @param key
-   *    the key should be a unique identifier for the request and its parameters
-   * @param service
-   *    the service that initiated the request
-   * @param scopeID
-   *    the id of an optional scope object
-   * @param paginationOptions
-   *    the pagination options (optional)
-   * @param sortOptions
-   *    the sort options (optional)
-   * @return Observable<RequestCacheEntry>
-   *    an observable of the RequestCacheEntry for this request
-   */
-  findAll(
-    key: string,
-    service: OpaqueToken,
-    scopeID?: string,
-    paginationOptions?: PaginationOptions,
-    sortOptions?: SortOptions
-  ): Observable<RequestCacheEntry> {
-      if (!this.has(key)) {
-        this.store.dispatch(new RequestCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
-      }
-    return this.get(key);
-  }
-
-  /**
-   * Start a new findById request
-   *
-   * This will send a new findById request to the backend,
-   * and store the request parameters and the fact that
-   * the request is pending
-   *
-   * @param key
-   *    the key should be a unique identifier for the request and its parameters
-   * @param service
-   *    the service that initiated the request
-   * @param resourceID
-   *    the ID of the resource to find
-   * @return Observable<RequestCacheEntry>
-   *    an observable of the RequestCacheEntry for this request
-   */
-  findById(
-    key: string,
-    service: OpaqueToken,
-    resourceID: string
-  ): Observable<RequestCacheEntry> {
-    if (!this.has(key)) {
-      this.store.dispatch(new RequestCacheFindByIDAction(key, service, resourceID));
-    }
-    return this.get(key);
-  }
-
-  /**
-   * Get an observable of the request with the specified key
-   *
-   * @param key
-   *    the key of the request to get
-   * @return Observable<RequestCacheEntry>
-   *    an observable of the RequestCacheEntry with the specified key
-   */
-  get(key: string): Observable<RequestCacheEntry> {
-    return this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
-      .filter(entry => this.isValid(entry))
-      .distinctUntilChanged()
-  }
-
-  /**
-   * Check whether the request with the specified key is cached
-   *
-   * @param key
-   *    the key of the request to check
-   * @return boolean
-   *    true if the request with the specified key is cached,
-   *    false otherwise
-   */
-  has(key: string): boolean {
-    let result: boolean;
-
-    this.store.select<RequestCacheEntry>('core', 'cache', 'request', key)
-      .take(1)
-      .subscribe(entry => result = this.isValid(entry));
-
-    return result;
-  }
-
-  /**
-   * Check whether a RequestCacheEntry should still be cached
-   *
-   * @param entry
-   *    the entry to check
-   * @return boolean
-   *    false if the entry is null, undefined, or its time to
-   *    live has been exceeded, true otherwise
-   */
-  private isValid(entry: RequestCacheEntry): boolean {
-    if (hasNoValue(entry)) {
-      return false;
-    }
-    else {
-      const timeOutdated = entry.timeAdded + entry.msToLive;
-      const isOutDated = new Date().getTime() > timeOutdated;
-      if (isOutDated) {
-        this.store.dispatch(new RequestCacheRemoveAction(entry.key));
-      }
-      return !isOutDated;
-    }
-  }
-
-}
diff --git a/src/app/core/cache/response-cache.actions.ts b/src/app/core/cache/response-cache.actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..45f78f10b7342b721acf7e6812adb1ed9af6aa36
--- /dev/null
+++ b/src/app/core/cache/response-cache.actions.ts
@@ -0,0 +1,69 @@
+import { Action } from "@ngrx/store";
+import { type } from "../../shared/ngrx/type";
+import { Response } from "./response-cache.models";
+
+/**
+ * The list of ResponseCacheAction type definitions
+ */
+export const ResponseCacheActionTypes = {
+  ADD: type('dspace/core/cache/response/ADD'),
+  REMOVE: type('dspace/core/cache/response/REMOVE'),
+  RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
+};
+
+export class ResponseCacheAddAction implements Action {
+  type = ResponseCacheActionTypes.ADD;
+  payload: {
+    key: string,
+    response: Response
+    timeAdded: number;
+    msToLive: number;
+  };
+
+  constructor(key: string, response: Response, timeAdded: number, msToLive: number) {
+    this.payload = { key, response, timeAdded, msToLive };
+  }
+}
+
+/**
+ * An ngrx action to remove a request from the cache
+ */
+export class ResponseCacheRemoveAction implements Action {
+  type = ResponseCacheActionTypes.REMOVE;
+  payload: string;
+
+  /**
+   * Create a new ResponseCacheRemoveAction
+   * @param key
+   *    The key of the request to remove
+   */
+  constructor(key: string) {
+    this.payload = key;
+  }
+}
+
+/**
+ * An ngrx action to reset the timeAdded property of all cached objects
+ */
+export class ResetResponseCacheTimestampsAction implements Action {
+  type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
+  payload: number;
+
+  /**
+   * Create a new ResetObjectCacheTimestampsAction
+   *
+   * @param newTimestamp
+   *    the new timeAdded all objects should get
+   */
+  constructor(newTimestamp: number) {
+    this.payload = newTimestamp;
+  }
+}
+
+/**
+ * A type to encompass all ResponseCacheActions
+ */
+export type ResponseCacheAction
+  = ResponseCacheAddAction
+  | ResponseCacheRemoveAction
+  | ResetResponseCacheTimestampsAction;
diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..741acf99a6ba97c7acd7945859ea02cffe49d163
--- /dev/null
+++ b/src/app/core/cache/response-cache.models.ts
@@ -0,0 +1,16 @@
+export class Response {
+  constructor(public isSuccessful: boolean) {}
+}
+
+export class SuccessResponse extends Response {
+  constructor(public resourceUUIDs: Array<String>) {
+    super(true);
+  }
+}
+
+export class ErrorResponse extends Response {
+  constructor(public errorMessage: string) {
+    super(false);
+  }
+}
+
diff --git a/src/app/core/cache/response-cache.reducer.spec.ts b/src/app/core/cache/response-cache.reducer.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b084842f7d1295f66663a73770d1a412c7223fc4
--- /dev/null
+++ b/src/app/core/cache/response-cache.reducer.spec.ts
@@ -0,0 +1,225 @@
+import { responseCacheReducer, ResponseCacheState } from "./response-cache.reducer";
+import {
+  ResponseCacheRemoveAction,
+  ResetResponseCacheTimestampsAction
+} from "./response-cache.actions";
+import deepFreeze = require("deep-freeze");
+
+class NullAction extends ResponseCacheRemoveAction {
+  type = null;
+  payload = null;
+
+  constructor() {
+    super(null);
+  }
+}
+
+// describe("responseCacheReducer", () => {
+//   const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
+//   const services = [new OpaqueToken('service1'), new OpaqueToken('service2')];
+//   const msToLive = 900000;
+//   const uuids = [
+//     "9e32a2e2-6b91-4236-a361-995ccdc14c60",
+//     "598ce822-c357-46f3-ab70-63724d02d6ad",
+//     "be8325f7-243b-49f4-8a4b-df2b793ff3b5"
+//   ];
+//   const resourceID = "9978";
+//   const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
+//   const sortOptions = { "field": "id", "direction": 0 };
+//   const testState = {
+//     [keys[0]]: {
+//       "key": keys[0],
+//       "service": services[0],
+//       "resourceUUIDs": [uuids[0], uuids[1]],
+//       "isLoading": false,
+//       "paginationOptions": paginationOptions,
+//       "sortOptions": sortOptions,
+//       "timeAdded": new Date().getTime(),
+//       "msToLive": msToLive
+//     },
+//     [keys[1]]: {
+//       "key": keys[1],
+//       "service": services[1],
+//       "resourceID": resourceID,
+//       "resourceUUIDs": [uuids[2]],
+//       "isLoading": false,
+//       "timeAdded": new Date().getTime(),
+//       "msToLive": msToLive
+//     }
+//   };
+//   deepFreeze(testState);
+//   const errorState: {} = {
+//     [keys[0]]: {
+//       errorMessage: 'error',
+//       resourceUUIDs: uuids
+//     }
+//   };
+//   deepFreeze(errorState);
+//
+//
+//   it("should return the current state when no valid actions have been made", () => {
+//     const action = new NullAction();
+//     const newState = responseCacheReducer(testState, action);
+//
+//     expect(newState).toEqual(testState);
+//   });
+//
+//   it("should start with an empty cache", () => {
+//     const action = new NullAction();
+//     const initialState = responseCacheReducer(undefined, action);
+//
+//     expect(initialState).toEqual(Object.create(null));
+//   });
+//
+//   describe("FIND_BY_ID", () => {
+//     const action = new ResponseCacheFindByIDAction(keys[0], services[0], resourceID);
+//
+//     it("should perform the action without affecting the previous state", () => {
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should add the request to the cache", () => {
+//       const state = Object.create(null);
+//       const newState = responseCacheReducer(state, action);
+//       expect(newState[keys[0]].key).toBe(keys[0]);
+//       expect(newState[keys[0]].service).toEqual(services[0]);
+//       expect(newState[keys[0]].resourceID).toBe(resourceID);
+//     });
+//
+//     it("should set responsePending to true", () => {
+//       const state = Object.create(null);
+//       const newState = responseCacheReducer(state, action);
+//       expect(newState[keys[0]].responsePending).toBe(true);
+//     });
+//
+//     it("should remove any previous error message or resourceUUID for the request", () => {
+//       const newState = responseCacheReducer(errorState, action);
+//       expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
+//       expect(newState[keys[0]].errorMessage).toBeUndefined();
+//     });
+//   });
+//
+//   describe("FIND_ALL", () => {
+//     const action = new ResponseCacheFindAllAction(keys[0], services[0], resourceID, paginationOptions, sortOptions);
+//
+//     it("should perform the action without affecting the previous state", () => {
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should add the request to the cache", () => {
+//       const state = Object.create(null);
+//       const newState = responseCacheReducer(state, action);
+//       expect(newState[keys[0]].key).toBe(keys[0]);
+//       expect(newState[keys[0]].service).toEqual(services[0]);
+//       expect(newState[keys[0]].scopeID).toBe(resourceID);
+//       expect(newState[keys[0]].paginationOptions).toEqual(paginationOptions);
+//       expect(newState[keys[0]].sortOptions).toEqual(sortOptions);
+//     });
+//
+//     it("should set responsePending to true", () => {
+//       const state = Object.create(null);
+//       const newState = responseCacheReducer(state, action);
+//       expect(newState[keys[0]].responsePending).toBe(true);
+//     });
+//
+//     it("should remove any previous error message or resourceUUIDs for the request", () => {
+//       const newState = responseCacheReducer(errorState, action);
+//       expect(newState[keys[0]].resourceUUIDs.length).toBe(0);
+//       expect(newState[keys[0]].errorMessage).toBeUndefined();
+//     });
+//   });
+//
+//   describe("SUCCESS", () => {
+//     const successUUIDs = [uuids[0], uuids[2]];
+//     const successTimeAdded = new Date().getTime();
+//     const successMsToLive = 5;
+//     const action = new ResponseCacheSuccessAction(keys[0], successUUIDs, successTimeAdded, successMsToLive);
+//
+//     it("should perform the action without affecting the previous state", () => {
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should add the response to the cached request", () => {
+//       const newState = responseCacheReducer(testState, action);
+//       expect(newState[keys[0]].resourceUUIDs).toBe(successUUIDs);
+//       expect(newState[keys[0]].timeAdded).toBe(successTimeAdded);
+//       expect(newState[keys[0]].msToLive).toBe(successMsToLive);
+//     });
+//
+//     it("should set responsePending to false", () => {
+//       const newState = responseCacheReducer(testState, action);
+//       expect(newState[keys[0]].responsePending).toBe(false);
+//     });
+//
+//     it("should remove any previous error message for the request", () => {
+//       const newState = responseCacheReducer(errorState, action);
+//       expect(newState[keys[0]].errorMessage).toBeUndefined();
+//     });
+//   });
+//
+//   describe("ERROR", () => {
+//     const errorMsg = 'errorMsg';
+//     const action = new ResponseCacheErrorAction(keys[0], errorMsg);
+//
+//     it("should perform the action without affecting the previous state", () => {
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should set an error message for the request", () => {
+//       const newState = responseCacheReducer(errorState, action);
+//       expect(newState[keys[0]].errorMessage).toBe(errorMsg);
+//     });
+//
+//     it("should set responsePending to false", () => {
+//       const newState = responseCacheReducer(testState, action);
+//       expect(newState[keys[0]].responsePending).toBe(false);
+//     });
+//   });
+//
+//   describe("REMOVE", () => {
+//     it("should perform the action without affecting the previous state", () => {
+//       const action = new ResponseCacheRemoveAction(keys[0]);
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should remove the specified request from the cache", () => {
+//       const action = new ResponseCacheRemoveAction(keys[0]);
+//       const newState = responseCacheReducer(testState, action);
+//       expect(testState[keys[0]]).not.toBeUndefined();
+//       expect(newState[keys[0]]).toBeUndefined();
+//     });
+//
+//     it("shouldn't do anything when the specified key isn't cached", () => {
+//       const wrongKey = "this isn't cached";
+//       const action = new ResponseCacheRemoveAction(wrongKey);
+//       const newState = responseCacheReducer(testState, action);
+//       expect(testState[wrongKey]).toBeUndefined();
+//       expect(newState).toEqual(testState);
+//     });
+//   });
+//
+//   describe("RESET_TIMESTAMPS", () => {
+//     const newTimeStamp = new Date().getTime();
+//     const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
+//
+//     it("should perform the action without affecting the previous state", () => {
+//       //testState has already been frozen above
+//       responseCacheReducer(testState, action);
+//     });
+//
+//     it("should set the timestamp of all requests in the cache", () => {
+//       const newState = responseCacheReducer(testState, action);
+//       Object.keys(newState).forEach((key) => {
+//         expect(newState[key].timeAdded).toEqual(newTimeStamp);
+//       });
+//     });
+//
+//   });
+//
+//
+// });
diff --git a/src/app/core/cache/response-cache.reducer.ts b/src/app/core/cache/response-cache.reducer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e0fa6f5eb47c1b173b05c397f5465f7d566da49
--- /dev/null
+++ b/src/app/core/cache/response-cache.reducer.ts
@@ -0,0 +1,112 @@
+import {
+  ResponseCacheAction, ResponseCacheActionTypes,
+  ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
+  ResponseCacheAddAction
+} from "./response-cache.actions";
+import { CacheEntry } from "./cache-entry";
+import { hasValue } from "../../shared/empty.util";
+import { Response } from "./response-cache.models";
+
+/**
+ * An entry in the ResponseCache
+ */
+export class ResponseCacheEntry implements CacheEntry {
+  key: string;
+  response: Response;
+  timeAdded: number;
+  msToLive: number;
+}
+
+/**
+ * The ResponseCache State
+ */
+export interface ResponseCacheState {
+  [key: string]: ResponseCacheEntry
+}
+
+// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
+const initialState = Object.create(null);
+
+/**
+ * The ResponseCache Reducer
+ *
+ * @param state
+ *    the current state
+ * @param action
+ *    the action to perform on the state
+ * @return ResponseCacheState
+ *    the new state
+ */
+export const responseCacheReducer = (state = initialState, action: ResponseCacheAction): ResponseCacheState => {
+  switch (action.type) {
+
+    case ResponseCacheActionTypes.ADD: {
+      return addToCache(state, <ResponseCacheAddAction> action);
+    }
+
+    case ResponseCacheActionTypes.REMOVE: {
+      return removeFromCache(state, <ResponseCacheRemoveAction> action);
+    }
+
+    case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
+      return resetResponseCacheTimestamps(state, <ResetResponseCacheTimestampsAction>action)
+    }
+
+    default: {
+      return state;
+    }
+  }
+};
+
+function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
+  return Object.assign({}, state, {
+    [action.payload.key]: {
+      key: action.payload.key,
+      response: action.payload.response,
+      timeAdded: action.payload.timeAdded,
+      msToLive: action.payload.msToLive
+    }
+  });
+}
+
+/**
+ * Remove a request from the cache
+ *
+ * @param state
+ *    the current state
+ * @param action
+ *    an ResponseCacheRemoveAction
+ * @return ResponseCacheState
+ *    the new state, with the request removed if it existed.
+ */
+function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
+  if (hasValue(state[action.payload])) {
+    let newCache = Object.assign({}, state);
+    delete newCache[action.payload];
+
+    return newCache;
+  }
+  else {
+    return state;
+  }
+}
+
+/**
+ * Set the timeAdded timestamp of every cached request to the specified value
+ *
+ * @param state
+ *    the current state
+ * @param action
+ *    a ResetResponseCacheTimestampsAction
+ * @return ResponseCacheState
+ *    the new state, with all timeAdded timestamps set to the specified value
+ */
+function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
+  let newState = Object.create(null);
+  Object.keys(state).forEach(key => {
+    newState[key] = Object.assign({}, state[key], {
+      timeAdded: action.payload
+    });
+  });
+  return newState;
+}
diff --git a/src/app/core/cache/response-cache.service.spec.ts b/src/app/core/cache/response-cache.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ec9da670a1a2233eba437332317af64092c81308
--- /dev/null
+++ b/src/app/core/cache/response-cache.service.spec.ts
@@ -0,0 +1,146 @@
+import { ResponseCacheService } from "./response-cache.service";
+import { Store } from "@ngrx/store";
+import { ResponseCacheState, ResponseCacheEntry } from "./response-cache.reducer";
+import { OpaqueToken } from "@angular/core";
+import { Observable } from "rxjs";
+
+// describe("ResponseCacheService", () => {
+//   let service: ResponseCacheService;
+//   let store: Store<ResponseCacheState>;
+//
+//   const keys = ["125c17f89046283c5f0640722aac9feb", "a06c3006a41caec5d635af099b0c780c"];
+//   const serviceTokens = [new OpaqueToken('service1'), new OpaqueToken('service2')];
+//   const resourceID = "9978";
+//   const paginationOptions = { "resultsPerPage": 10, "currentPage": 1 };
+//   const sortOptions = { "field": "id", "direction": 0 };
+//   const timestamp = new Date().getTime();
+//   const validCacheEntry = (key) => {
+//     return {
+//       key: key,
+//       timeAdded: timestamp,
+//       msToLive: 24 * 60 * 60 * 1000 // a day
+//     }
+//   };
+//   const invalidCacheEntry = (key) => {
+//     return {
+//       key: key,
+//       timeAdded: 0,
+//       msToLive: 0
+//     }
+//   };
+//
+//   beforeEach(() => {
+//     store = new Store<ResponseCacheState>(undefined, undefined, undefined);
+//     spyOn(store, 'dispatch');
+//     service = new ResponseCacheService(store);
+//     spyOn(window, 'Date').and.returnValue({ getTime: () => timestamp });
+//   });
+//
+//   describe("findAll", () => {
+//     beforeEach(() => {
+//       spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
+//     });
+//     describe("if the key isn't cached", () => {
+//       beforeEach(() => {
+//           spyOn(service, "has").and.returnValue(false);
+//       });
+//       it("should dispatch a FIND_ALL action with the key, service, scopeID, paginationOptions and sortOptions", () => {
+//         service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
+//         expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindAllAction(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions))
+//       });
+//       it("should return an observable of the newly cached request with the specified key", () => {
+//         let result: ResponseCacheEntry;
+//         service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
+//         expect(result.key).toEqual(keys[0]);
+//       });
+//     });
+//     describe("if the key is already cached", () => {
+//       beforeEach(() => {
+//           spyOn(service, "has").and.returnValue(true);
+//       });
+//       it("shouldn't dispatch anything", () => {
+//         service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions);
+//         expect(store.dispatch).not.toHaveBeenCalled();
+//       });
+//       it("should return an observable of the existing cached request with the specified key", () => {
+//         let result: ResponseCacheEntry;
+//         service.findAll(keys[0], serviceTokens[0], resourceID, paginationOptions, sortOptions).take(1).subscribe(entry => result = entry);
+//         expect(result.key).toEqual(keys[0]);
+//       });
+//     });
+//   });
+//
+//   describe("findById", () => {
+//     beforeEach(() => {
+//       spyOn(service, "get").and.callFake((key) => Observable.of({key: key}));
+//     });
+//     describe("if the key isn't cached", () => {
+//       beforeEach(() => {
+//           spyOn(service, "has").and.returnValue(false);
+//       });
+//       it("should dispatch a FIND_BY_ID action with the key, service, and resourceID", () => {
+//         service.findById(keys[0], serviceTokens[0], resourceID);
+//         expect(store.dispatch).toHaveBeenCalledWith(new ResponseCacheFindByIDAction(keys[0], serviceTokens[0], resourceID))
+//       });
+//       it("should return an observable of the newly cached request with the specified key", () => {
+//         let result: ResponseCacheEntry;
+//         service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
+//         expect(result.key).toEqual(keys[0]);
+//       });
+//     });
+//     describe("if the key is already cached", () => {
+//       beforeEach(() => {
+//           spyOn(service, "has").and.returnValue(true);
+//       });
+//       it("shouldn't dispatch anything", () => {
+//         service.findById(keys[0], serviceTokens[0], resourceID);
+//         expect(store.dispatch).not.toHaveBeenCalled();
+//       });
+//       it("should return an observable of the existing cached request with the specified key", () => {
+//         let result: ResponseCacheEntry;
+//         service.findById(keys[0], serviceTokens[0], resourceID).take(1).subscribe(entry => result = entry);
+//         expect(result.key).toEqual(keys[0]);
+//       });
+//     });
+//   });
+//
+//   describe("get", () => {
+//     it("should return an observable of the cached request with the specified key", () => {
+//       spyOn(store, "select").and.callFake((...args:Array<any>) => {
+//         return Observable.of(validCacheEntry(args[args.length - 1]));
+//       });
+//
+//       let testObj: ResponseCacheEntry;
+//       service.get(keys[1]).take(1).subscribe(entry => testObj = entry);
+//       expect(testObj.key).toEqual(keys[1]);
+//     });
+//
+//     it("should not return a cached request that has exceeded its time to live", () => {
+//       spyOn(store, "select").and.callFake((...args:Array<any>) => {
+//         return Observable.of(invalidCacheEntry(args[args.length - 1]));
+//       });
+//
+//       let getObsHasFired = false;
+//       const subscription = service.get(keys[1]).subscribe(entry => getObsHasFired = true);
+//       expect(getObsHasFired).toBe(false);
+//       subscription.unsubscribe();
+//     });
+//   });
+//
+//   describe("has", () => {
+//     it("should return true if the request with the supplied key is cached and still valid", () => {
+//       spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1])));
+//       expect(service.has(keys[1])).toBe(true);
+//     });
+//
+//     it("should return false if the request with the supplied key isn't cached", () => {
+//       spyOn(store, 'select').and.returnValue(Observable.of(undefined));
+//       expect(service.has(keys[1])).toBe(false);
+//     });
+//
+//     it("should return false if the request with the supplied key is cached but has exceeded its time to live", () => {
+//       spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
+//       expect(service.has(keys[1])).toBe(false);
+//     });
+//   });
+// });
diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..17d9ed10913b35e8829ad3095c13f29f681df124
--- /dev/null
+++ b/src/app/core/cache/response-cache.service.ts
@@ -0,0 +1,89 @@
+import { Injectable } from "@angular/core";
+import { Store } from "@ngrx/store";
+import {
+  ResponseCacheState, ResponseCacheEntry
+} from "./response-cache.reducer";
+import { Observable } from "rxjs";
+import { hasNoValue } from "../../shared/empty.util";
+import {
+  ResponseCacheRemoveAction,
+  ResponseCacheAddAction
+} from "./response-cache.actions";
+import { Response } from "./response-cache.models";
+
+/**
+ * A service to interact with the response cache
+ */
+@Injectable()
+export class ResponseCacheService {
+  constructor(
+    private store: Store<ResponseCacheState>
+  ) {}
+
+  add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> {
+      if (!this.has(key)) {
+        // this.store.dispatch(new ResponseCacheFindAllAction(key, service, scopeID, paginationOptions, sortOptions));
+        this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
+      }
+    return this.get(key);
+  }
+
+  /**
+   * Get an observable of the response with the specified key
+   *
+   * @param key
+   *    the key of the response to get
+   * @return Observable<ResponseCacheEntry>
+   *    an observable of the ResponseCacheEntry with the specified key
+   */
+  get(key: string): Observable<ResponseCacheEntry> {
+    return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
+      .filter(entry => this.isValid(entry))
+      .distinctUntilChanged()
+  }
+
+  /**
+   * Check whether the response with the specified key is cached
+   *
+   * @param key
+   *    the key of the response to check
+   * @return boolean
+   *    true if the response with the specified key is cached,
+   *    false otherwise
+   */
+  has(key: string): boolean {
+    let result: boolean;
+
+    this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
+      .take(1)
+      .subscribe(entry => {
+        result = this.isValid(entry);
+      });
+
+    return result;
+  }
+
+  /**
+   * Check whether a ResponseCacheEntry should still be cached
+   *
+   * @param entry
+   *    the entry to check
+   * @return boolean
+   *    false if the entry is null, undefined, or its time to
+   *    live has been exceeded, true otherwise
+   */
+  private isValid(entry: ResponseCacheEntry): boolean {
+    if (hasNoValue(entry)) {
+      return false;
+    }
+    else {
+      const timeOutdated = entry.timeAdded + entry.msToLive;
+      const isOutDated = new Date().getTime() > timeOutdated;
+      if (isOutDated) {
+        this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
+      }
+      return !isOutDated;
+    }
+  }
+
+}
diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts
index b2d6c95ad5dde257b39f6127b1374610a7cc2407..ef9da245dfc030e7f6671842163d48489190315b 100644
--- a/src/app/core/core.effects.ts
+++ b/src/app/core/core.effects.ts
@@ -1,12 +1,11 @@
 import { EffectsModule } from "@ngrx/effects";
-import { CollectionDataEffects } from "./data-services/collection-data.effects";
-import { ItemDataEffects } from "./data-services/item-data.effects";
-import { ObjectCacheEffects } from "./data-services/object-cache.effects";
-import { RequestCacheEffects } from "./data-services/request-cache.effects";
+import { ObjectCacheEffects } from "./data/object-cache.effects";
+import { RequestCacheEffects } from "./data/request-cache.effects";
+import { HrefIndexEffects } from "./index/href-index.effects";
+import { RequestEffects } from "./data/request.effects";
 
 export const coreEffects = [
-  EffectsModule.run(CollectionDataEffects),
-  EffectsModule.run(ItemDataEffects),
-  EffectsModule.run(RequestCacheEffects),
+  EffectsModule.run(RequestEffects),
   EffectsModule.run(ObjectCacheEffects),
+  EffectsModule.run(HrefIndexEffects),
 ];
diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts
index 5cc690ed80eb87ad9801acbea88c9d380923fb13..cdd474a909224b193d121441a9c772947a3c148d 100644
--- a/src/app/core/core.module.ts
+++ b/src/app/core/core.module.ts
@@ -1,15 +1,18 @@
 import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core';
 import { CommonModule } from '@angular/common';
-
 import { SharedModule } from "../shared/shared.module";
+
 import { isNotEmpty } from "../shared/empty.util";
 import { FooterComponent } from "./footer/footer.component";
 import { DSpaceRESTv2Service } from "./dspace-rest-v2/dspace-rest-v2.service";
 import { ObjectCacheService } from "./cache/object-cache.service";
-import { RequestCacheService } from "./cache/request-cache.service";
-import { CollectionDataService } from "./data-services/collection-data.service";
-import { ItemDataService } from "./data-services/item-data.service";
-import { PaginationOptions } from "./shared/pagination-options.model";
+import { ResponseCacheService } from "./cache/response-cache.service";
+import { CollectionDataService } from "./data/collection-data.service";
+import { ItemDataService } from "./data/item-data.service";
+import { RequestService } from "./data/request.service";
+import { RemoteDataBuildService } from "./cache/builders/remote-data-build.service";
+import { CommunityDataService } from "./data/community-data.service";
+import { PaginationOptions } from "./cache/models/pagination-options.model";
 
 const IMPORTS = [
   CommonModule,
@@ -25,12 +28,14 @@ const EXPORTS = [
 ];
 
 const PROVIDERS = [
+  CommunityDataService,
   CollectionDataService,
   ItemDataService,
   DSpaceRESTv2Service,
   ObjectCacheService,
   PaginationOptions,
-  RequestCacheService
+  RequestService,
+  RemoteDataBuildService
 ];
 
 @NgModule({
diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts
index 71f25ee0b0c0c648999e4fa66bf37b5a981331f7..556866dbc4157db0e8da02c4e6a7143c7c23487f 100644
--- a/src/app/core/core.reducers.ts
+++ b/src/app/core/core.reducers.ts
@@ -1,12 +1,18 @@
 import { combineReducers } from "@ngrx/store";
 import { CacheState, cacheReducer } from "./cache/cache.reducers";
+import { IndexState, indexReducer } from "./index/index.reducers";
+import { DataState, dataReducer } from "./data/data.reducers";
 
 export interface CoreState {
-  cache: CacheState
+  cache: CacheState,
+  index: IndexState,
+  data: DataState
 }
 
 export const reducers = {
-  cache: cacheReducer
+  cache: cacheReducer,
+  index: indexReducer,
+  data: dataReducer
 };
 
 export function coreReducer(state: any, action: any) {
diff --git a/src/app/core/data-services/collection-data.effects.ts b/src/app/core/data-services/collection-data.effects.ts
deleted file mode 100644
index 9586940defe82196a9d4bf4abef82db77f155dd8..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/collection-data.effects.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Inject, Injectable } from "@angular/core";
-import { DataEffects } from "./data.effects";
-import { Serializer } from "../serializer";
-import { Collection } from "../shared/collection.model";
-import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
-import { Actions, Effect } from "@ngrx/effects";
-import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
-import { CollectionDataService } from "./collection-data.service";
-
-import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
-
-@Injectable()
-export class CollectionDataEffects extends DataEffects<Collection> {
-  constructor(
-    @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig,
-    actions$: Actions,
-    restApi: DSpaceRESTv2Service,
-    cache: ObjectCacheService,
-    dataService: CollectionDataService
-  ) {
-    super(EnvConfig, actions$, restApi, cache, dataService);
-  }
-
-  protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
-    return '/collections';
-  }
-
-  protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
-    return `/collections/${action.payload.resourceID}`;
-  }
-
-  protected getSerializer(): Serializer<Collection> {
-    return new DSpaceRESTv2Serializer(Collection);
-  }
-
-  @Effect() findAll$ = this.findAll;
-
-  @Effect() findById$ = this.findById;
-}
diff --git a/src/app/core/data-services/collection-data.service.ts b/src/app/core/data-services/collection-data.service.ts
deleted file mode 100644
index cc850900db4c0d5616787145f09d922de6dd51d9..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/collection-data.service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Injectable, OpaqueToken } from "@angular/core";
-import { DataService } from "./data.service";
-import { Collection } from "../shared/collection.model";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { RequestCacheService } from "../cache/request-cache.service";
-
-@Injectable()
-export class CollectionDataService extends DataService<Collection> {
-  serviceName = new OpaqueToken('CollectionDataService');
-
-  constructor(
-    protected objectCache: ObjectCacheService,
-    protected requestCache: RequestCacheService,
-  ) {
-    super(Collection);
-  }
-
-}
diff --git a/src/app/core/data-services/data.effects.ts b/src/app/core/data-services/data.effects.ts
deleted file mode 100644
index 107ad7eca3a4826778044bc6c01c6fe7a4c8421f..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/data.effects.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { Inject } from "@angular/core";
-import { Actions } from "@ngrx/effects";
-import { Observable } from "rxjs";
-import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
-import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { CacheableObject } from "../cache/object-cache.reducer";
-import { Serializer } from "../serializer";
-import {
-  RequestCacheActionTypes, RequestCacheFindAllAction, RequestCacheSuccessAction,
-  RequestCacheErrorAction, RequestCacheFindByIDAction
-} from "../cache/request-cache.actions";
-import { DataService } from "./data.service";
-import { hasNoValue } from "../../shared/empty.util";
-
-import { GlobalConfig } from '../../../config';
-
-export abstract class DataEffects<T extends CacheableObject> {
-  protected abstract getFindAllEndpoint(action: RequestCacheFindAllAction): string;
-  protected abstract getFindByIdEndpoint(action: RequestCacheFindByIDAction): string;
-  protected abstract getSerializer(): Serializer<T>;
-
-  constructor(
-    private EnvConfig: GlobalConfig,
-    private actions$: Actions,
-    private restApi: DSpaceRESTv2Service,
-    private objectCache: ObjectCacheService,
-    private dataService: DataService<T>
-  ) { }
-
-  // TODO, results of a findall aren't retrieved from cache yet
-  protected findAll = this.actions$
-    .ofType(RequestCacheActionTypes.FIND_ALL)
-    .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName)
-    .flatMap((action: RequestCacheFindAllAction) => {
-      //TODO scope, pagination, sorting -> when we know how that works in rest
-      return this.restApi.get(this.getFindAllEndpoint(action))
-        .map((data: DSpaceRESTV2Response) => this.getSerializer().deserializeArray(data))
-        .do((ts: T[]) => {
-          ts.forEach((t) => {
-            if (hasNoValue(t) || hasNoValue(t.uuid)) {
-              throw new Error('The server returned an invalid object');
-            }
-            this.objectCache.add(t, this.EnvConfig.cache.msToLive);
-          });
-        })
-        .map((ts: Array<T>) => ts.map(t => t.uuid))
-        .map((ids: Array<string>) => new RequestCacheSuccessAction(action.payload.key, ids, new Date().getTime(), this.EnvConfig.cache.msToLive))
-        .catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
-    });
-
-  protected findById = this.actions$
-    .ofType(RequestCacheActionTypes.FIND_BY_ID)
-    .filter((action: RequestCacheFindAllAction) => action.payload.service === this.dataService.serviceName)
-    .flatMap((action: RequestCacheFindByIDAction) => {
-      return this.restApi.get(this.getFindByIdEndpoint(action))
-        .map((data: DSpaceRESTV2Response) => this.getSerializer().deserialize(data))
-        .do((t: T) => {
-          if (hasNoValue(t) || hasNoValue(t.uuid)) {
-            throw new Error('The server returned an invalid object');
-          }
-          this.objectCache.add(t, this.EnvConfig.cache.msToLive);
-        })
-        .map((t: T) => new RequestCacheSuccessAction(action.payload.key, [t.uuid], new Date().getTime(), this.EnvConfig.cache.msToLive))
-        .catch((error: Error) => Observable.of(new RequestCacheErrorAction(action.payload.key, error.message)));
-    });
-
-}
diff --git a/src/app/core/data-services/data.service.ts b/src/app/core/data-services/data.service.ts
deleted file mode 100644
index ddbfa03eb44009262e3483031032c4649c0260fd..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/data.service.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { OpaqueToken } from "@angular/core";
-import { Observable } from "rxjs";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { RequestCacheService } from "../cache/request-cache.service";
-import { CacheableObject } from "../cache/object-cache.reducer";
-import { ParamHash } from "../shared/param-hash";
-import { isNotEmpty } from "../../shared/empty.util";
-import { GenericConstructor } from "../shared/generic-constructor";
-import { RemoteData } from "./remote-data";
-
-export abstract class DataService<T extends CacheableObject> {
-  abstract serviceName: OpaqueToken;
-  protected abstract objectCache: ObjectCacheService;
-  protected abstract requestCache: RequestCacheService;
-
-  constructor(private modelType: GenericConstructor<T>) {
-
-  }
-
-  findAll(scopeID?: string): RemoteData<Array<T>> {
-    const key = new ParamHash(this.serviceName, 'findAll', scopeID).toString();
-    const requestCacheObs = this.requestCache.findAll(key, this.serviceName, scopeID);
-    return new RemoteData(
-      requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
-      requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
-      requestCacheObs
-        .map(entry => entry.resourceUUIDs)
-        .flatMap((resourceUUIDs: Array<string>) => {
-          // use those IDs to fetch the actual objects from the ObjectCache
-          return this.objectCache.getList<T>(resourceUUIDs, this.modelType);
-        }).distinctUntilChanged()
-    );
-  }
-
-  findById(id: string): RemoteData<T> {
-    const key = new ParamHash(this.serviceName, 'findById', id).toString();
-    const requestCacheObs = this.requestCache.findById(key, this.serviceName, id);
-    return new RemoteData(
-      requestCacheObs.map(entry => entry.isLoading).distinctUntilChanged(),
-      requestCacheObs.map(entry => entry.errorMessage).distinctUntilChanged(),
-      requestCacheObs
-        .map(entry => entry.resourceUUIDs)
-        .flatMap((resourceUUIDs: Array<string>) => {
-          if (isNotEmpty(resourceUUIDs)) {
-            return this.objectCache.get<T>(resourceUUIDs[0], this.modelType);
-          }
-          else {
-            return Observable.of(undefined);
-          }
-        }).distinctUntilChanged()
-    );
-  }
-
-}
diff --git a/src/app/core/data-services/item-data.effects.ts b/src/app/core/data-services/item-data.effects.ts
deleted file mode 100644
index 8c140c8398b3fa0765899020ad7f3d57c6731dc9..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/item-data.effects.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Inject, Injectable } from "@angular/core";
-import { DataEffects } from "./data.effects";
-import { Serializer } from "../serializer";
-import { Item } from "../shared/item.model";
-import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
-import { Actions, Effect } from "@ngrx/effects";
-import { RequestCacheFindAllAction, RequestCacheFindByIDAction } from "../cache/request-cache.actions";
-import { ItemDataService } from "./item-data.service";
-
-import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
-
-@Injectable()
-export class ItemDataEffects extends DataEffects<Item> {
-  constructor(
-    @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig,
-    actions$: Actions,
-    restApi: DSpaceRESTv2Service,
-    cache: ObjectCacheService,
-    dataService: ItemDataService
-  ) {
-    super(EnvConfig, actions$, restApi, cache, dataService);
-  }
-
-  protected getFindAllEndpoint(action: RequestCacheFindAllAction): string {
-    return '/items';
-  }
-
-  protected getFindByIdEndpoint(action: RequestCacheFindByIDAction): string {
-    return `/items/${action.payload.resourceID}`;
-  }
-
-  protected getSerializer(): Serializer<Item> {
-    return new DSpaceRESTv2Serializer(Item);
-  }
-
-  @Effect() findAll$ = this.findAll;
-
-  @Effect() findById$ = this.findById;
-}
diff --git a/src/app/core/data-services/item-data.service.ts b/src/app/core/data-services/item-data.service.ts
deleted file mode 100644
index f3c8fd83af184e7ac5c9a95314cf446fc567123e..0000000000000000000000000000000000000000
--- a/src/app/core/data-services/item-data.service.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Injectable, OpaqueToken } from "@angular/core";
-import { DataService } from "./data.service";
-import { Item } from "../shared/item.model";
-import { ObjectCacheService } from "../cache/object-cache.service";
-import { RequestCacheService } from "../cache/request-cache.service";
-
-@Injectable()
-export class ItemDataService extends DataService<Item> {
-  serviceName = new OpaqueToken('ItemDataService');
-
-  constructor(
-    protected objectCache: ObjectCacheService,
-    protected requestCache: RequestCacheService,
-  ) {
-    super(Item);
-  }
-
-}
diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..232345d2be1e963fff3f1486439753847e753470
--- /dev/null
+++ b/src/app/core/data/collection-data.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@angular/core";
+import { DataService } from "./data.service";
+import { Collection } from "../shared/collection.model";
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { Store } from "@ngrx/store";
+import { NormalizedCollection } from "../cache/models/normalized-collection.model";
+import { CoreState } from "../core.reducers";
+import { RequestService } from "./request.service";
+import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
+
+@Injectable()
+export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
+  protected endpoint = '/collections';
+
+  constructor(
+    protected objectCache: ObjectCacheService,
+    protected responseCache: ResponseCacheService,
+    protected requestService: RequestService,
+    protected rdbService: RemoteDataBuildService,
+    protected store: Store<CoreState>
+  ) {
+      super(NormalizedCollection);
+  }
+
+}
diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6e635974071dc504154787b6ec6d4d73afcf9e41
--- /dev/null
+++ b/src/app/core/data/community-data.service.ts
@@ -0,0 +1,26 @@
+import { Injectable } from "@angular/core";
+import { DataService } from "./data.service";
+import { Community } from "../shared/community.model";
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { Store } from "@ngrx/store";
+import { NormalizedCommunity } from "../cache/models/normalized-community.model";
+import { CoreState } from "../core.reducers";
+import { RequestService } from "./request.service";
+import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
+
+@Injectable()
+export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
+  protected endpoint = '/communities';
+
+  constructor(
+    protected objectCache: ObjectCacheService,
+    protected responseCache: ResponseCacheService,
+    protected requestService: RequestService,
+    protected rdbService: RemoteDataBuildService,
+    protected store: Store<CoreState>
+  ) {
+      super(NormalizedCommunity);
+  }
+
+}
diff --git a/src/app/core/data/data.reducers.ts b/src/app/core/data/data.reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..af7d2697ccee4fa2dc3d1eae8b097c1ad71512c7
--- /dev/null
+++ b/src/app/core/data/data.reducers.ts
@@ -0,0 +1,14 @@
+import { combineReducers } from "@ngrx/store";
+import { RequestState, requestReducer } from "./request.reducer";
+
+export interface DataState {
+  request: RequestState
+}
+
+export const reducers = {
+  request: requestReducer
+};
+
+export function dataReducer(state: any, action: any) {
+  return combineReducers(reducers)(state, action);
+}
diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..809ff799b3e74c14ef43d9cc1ac13f52f8616531
--- /dev/null
+++ b/src/app/core/data/data.service.ts
@@ -0,0 +1,70 @@
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { CacheableObject } from "../cache/object-cache.reducer";
+import { hasValue } from "../../shared/empty.util";
+import { RemoteData } from "./remote-data";
+import { FindAllRequest, FindByIDRequest, Request } from "./request.models";
+import { Store } from "@ngrx/store";
+import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
+import { CoreState } from "../core.reducers";
+import { RequestService } from "./request.service";
+import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
+import { GenericConstructor } from "../shared/generic-constructor";
+
+export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
+  protected abstract objectCache: ObjectCacheService;
+  protected abstract responseCache: ResponseCacheService;
+  protected abstract requestService: RequestService;
+  protected abstract rdbService: RemoteDataBuildService;
+  protected abstract store: Store<CoreState>;
+  protected abstract endpoint: string;
+
+  constructor(private normalizedResourceType: GenericConstructor<TNormalized>) {
+
+  }
+
+  protected getFindAllHref(scopeID?): string {
+    let result = this.endpoint;
+    if (hasValue(scopeID)) {
+      result += `?scope=${scopeID}`
+    }
+    return result;
+  }
+
+  findAll(scopeID?: string): RemoteData<Array<TDomain>> {
+    const href = this.getFindAllHref(scopeID);
+    if (!this.responseCache.has(href) && !this.requestService.isPending(href)) {
+      const request = new FindAllRequest(href, this.normalizedResourceType, scopeID);
+      this.store.dispatch(new RequestConfigureAction(request));
+      this.store.dispatch(new RequestExecuteAction(href));
+    }
+    return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType);
+    // return this.rdbService.buildList(href);
+  }
+
+  protected getFindByIDHref(resourceID): string {
+    return `${this.endpoint}/${resourceID}`;
+  }
+
+  findById(id: string): RemoteData<TDomain> {
+    const href = this.getFindByIDHref(id);
+    if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) {
+      const request = new FindByIDRequest(href, this.normalizedResourceType, id);
+      this.store.dispatch(new RequestConfigureAction(request));
+      this.store.dispatch(new RequestExecuteAction(href));
+    }
+    return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
+    // return this.rdbService.buildSingle(href);
+  }
+
+  findByHref(href: string): RemoteData<TDomain> {
+    if (!this.objectCache.hasBySelfLink(href) && !this.requestService.isPending(href)) {
+      const request = new Request(href, this.normalizedResourceType);
+      this.store.dispatch(new RequestConfigureAction(request));
+      this.store.dispatch(new RequestExecuteAction(href));
+    }
+    return this.rdbService.buildSingle<TNormalized, TDomain>(href, this.normalizedResourceType);
+    // return this.rdbService.buildSingle(href));
+  }
+
+}
diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc13999f37f376df06279a9c8ea656ee551bdaf3
--- /dev/null
+++ b/src/app/core/data/item-data.service.ts
@@ -0,0 +1,25 @@
+import { Injectable } from "@angular/core";
+import { DataService } from "./data.service";
+import { Item } from "../shared/item.model";
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { Store } from "@ngrx/store";
+import { CoreState } from "../core.reducers";
+import { NormalizedItem } from "../cache/models/normalized-item.model";
+import { RequestService } from "./request.service";
+import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
+
+@Injectable()
+export class ItemDataService extends DataService<NormalizedItem, Item> {
+  protected endpoint = '/items';
+
+  constructor(
+    protected objectCache: ObjectCacheService,
+    protected responseCache: ResponseCacheService,
+    protected requestService: RequestService,
+    protected rdbService: RemoteDataBuildService,
+    protected store: Store<CoreState>
+) {
+    super(NormalizedItem);
+  }
+}
diff --git a/src/app/core/data-services/object-cache.effects.ts b/src/app/core/data/object-cache.effects.ts
similarity index 81%
rename from src/app/core/data-services/object-cache.effects.ts
rename to src/app/core/data/object-cache.effects.ts
index 26f13ea1b51f5ed69e080a4a7640b71697092605..af5a0658a3fb8d82c50b91b4f47f527a00e47999 100644
--- a/src/app/core/data-services/object-cache.effects.ts
+++ b/src/app/core/data/object-cache.effects.ts
@@ -2,15 +2,12 @@ import { Injectable } from "@angular/core";
 import { Actions, Effect } from "@ngrx/effects";
 import { StoreActionTypes } from "../../store.actions";
 import { ResetObjectCacheTimestampsAction } from "../cache/object-cache.actions";
-import { Store } from "@ngrx/store";
-import { ObjectCacheState } from "../cache/object-cache.reducer";
 
 @Injectable()
 export class ObjectCacheEffects {
 
   constructor(
-    private actions$: Actions,
-    private store: Store<ObjectCacheState>
+    private actions$: Actions
   ) { }
 
   /**
diff --git a/src/app/core/data-services/remote-data.ts b/src/app/core/data/remote-data.ts
similarity index 73%
rename from src/app/core/data-services/remote-data.ts
rename to src/app/core/data/remote-data.ts
index 1b9ff177ef317a6458b8dea4f900481dd6eeb38e..7fa02bf25c0908bd38b95fe9b38d03f672e30434 100644
--- a/src/app/core/data-services/remote-data.ts
+++ b/src/app/core/data/remote-data.ts
@@ -1,8 +1,6 @@
 import { Observable } from "rxjs";
-import { hasValue } from "../../shared/empty.util";
 
 export enum RemoteDataState {
-  //TODO RequestPending will never happen: implement it in the store & DataEffects.
   RequestPending,
   ResponsePending,
   Failed,
@@ -10,12 +8,14 @@ export enum RemoteDataState {
 }
 
 /**
- * A class to represent the state of
+ * A class to represent the state of a remote resource
  */
 export class RemoteData<T> {
-
   constructor(
-    private storeLoading: Observable<boolean>,
+    public self: string,
+    private requestPending: Observable<boolean>,
+    private responsePending: Observable<boolean>,
+    private isSuccessFul: Observable<boolean>,
     public errorMessage: Observable<string>,
     public payload: Observable<T>
   ) {
@@ -23,13 +23,17 @@ export class RemoteData<T> {
 
   get state(): Observable<RemoteDataState> {
     return Observable.combineLatest(
-      this.storeLoading,
-      this.errorMessage.map(msg => hasValue(msg)),
-      (storeLoading, hasMsg) => {
-        if (storeLoading) {
+      this.requestPending,
+      this.responsePending,
+      this.isSuccessFul,
+      (requestPending, responsePending, isSuccessFul) => {
+        if (requestPending) {
+          return RemoteDataState.RequestPending
+        }
+        else if (responsePending) {
           return RemoteDataState.ResponsePending
         }
-        else if (hasMsg) {
+        else if (!isSuccessFul) {
           return RemoteDataState.Failed
         }
         else {
diff --git a/src/app/core/data-services/request-cache.effects.ts b/src/app/core/data/request-cache.effects.ts
similarity index 72%
rename from src/app/core/data-services/request-cache.effects.ts
rename to src/app/core/data/request-cache.effects.ts
index b8dde5115975c2dd8510239b484d73a9c2b2ec86..3c650d95f133042e73915dbbfa9924330593cb55 100644
--- a/src/app/core/data-services/request-cache.effects.ts
+++ b/src/app/core/data/request-cache.effects.ts
@@ -1,16 +1,15 @@
-import { Injectable } from "@angular/core";
+import { Injectable, Inject } from "@angular/core";
 import { Actions, Effect } from "@ngrx/effects";
-import { ResetRequestCacheTimestampsAction } from "../cache/request-cache.actions";
-import { Store } from "@ngrx/store";
-import { RequestCacheState } from "../cache/request-cache.reducer";
 import { ObjectCacheActionTypes } from "../cache/object-cache.actions";
+import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
+import { ResetResponseCacheTimestampsAction } from "../cache/response-cache.actions";
 
 @Injectable()
 export class RequestCacheEffects {
 
   constructor(
+    @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
     private actions$: Actions,
-    private store: Store<RequestCacheState>
   ) { }
 
   /**
@@ -31,6 +30,5 @@ export class RequestCacheEffects {
    */
   @Effect() fixTimestampsOnRehydrate = this.actions$
     .ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
-    .map(() => new ResetRequestCacheTimestampsAction(new Date().getTime()));
-
+    .map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
 }
diff --git a/src/app/core/data/request.actions.ts b/src/app/core/data/request.actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16ce1963bd789fae91f7a27ae33caf681b280d33
--- /dev/null
+++ b/src/app/core/data/request.actions.ts
@@ -0,0 +1,59 @@
+import { Action } from "@ngrx/store";
+import { type } from "../../shared/ngrx/type";
+import { CacheableObject } from "../cache/object-cache.reducer";
+import { Request } from "./request.models";
+
+/**
+ * The list of RequestAction type definitions
+ */
+export const RequestActionTypes = {
+  CONFIGURE: type('dspace/core/data/request/CONFIGURE'),
+  EXECUTE: type('dspace/core/data/request/EXECUTE'),
+  COMPLETE: type('dspace/core/data/request/COMPLETE')
+};
+
+export class RequestConfigureAction implements Action {
+  type = RequestActionTypes.CONFIGURE;
+  payload: Request<CacheableObject>;
+
+  constructor(
+    request: Request<CacheableObject>
+  ) {
+    this.payload = request;
+  }
+}
+
+export class RequestExecuteAction implements Action {
+  type = RequestActionTypes.EXECUTE;
+  payload: string;
+
+  constructor(key: string) {
+    this.payload = key
+  }
+}
+
+/**
+ * An ngrx action to indicate a response was returned
+ */
+export class RequestCompleteAction implements Action {
+  type = RequestActionTypes.COMPLETE;
+  payload: string;
+
+  /**
+   * Create a new RequestCompleteAction
+   *
+   * @param key
+   *    the key under which  this request is stored,
+   */
+  constructor(key: string) {
+    this.payload = key;
+  }
+}
+
+/**
+ * A type to encompass all RequestActions
+ */
+export type RequestAction
+  = RequestConfigureAction
+  | RequestExecuteAction
+  | RequestCompleteAction;
diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e5d887626ed5d8cfb2809aac1e14107bc226d326
--- /dev/null
+++ b/src/app/core/data/request.effects.ts
@@ -0,0 +1,71 @@
+import { Injectable, Inject } from "@angular/core";
+import { Actions, Effect } from "@ngrx/effects";
+import { Store } from "@ngrx/store";
+import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
+import { DSpaceRESTv2Serializer } from "../dspace-rest-v2/dspace-rest-v2.serializer";
+import { CacheableObject } from "../cache/object-cache.reducer";
+import { Observable } from "rxjs";
+import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
+import { hasNoValue } from "../../shared/empty.util";
+import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
+import { RequestState, RequestEntry } from "./request.reducer";
+import {
+  RequestActionTypes, RequestExecuteAction,
+  RequestCompleteAction
+} from "./request.actions";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { RequestService } from "./request.service";
+
+@Injectable()
+export class RequestEffects {
+
+  constructor(
+    @Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig,
+    private actions$: Actions,
+    private restApi: DSpaceRESTv2Service,
+    private objectCache: ObjectCacheService,
+    private responseCache: ResponseCacheService,
+    protected requestService: RequestService,
+    private store: Store<RequestState>
+  ) { }
+
+  @Effect() execute = this.actions$
+    .ofType(RequestActionTypes.EXECUTE)
+    .flatMap((action: RequestExecuteAction) => {
+      return this.requestService.get(action.payload)
+        .take(1);
+    })
+    .flatMap((entry: RequestEntry) => {
+      const [ifArray, ifNotArray] = this.restApi.get(entry.request.href)
+        .share() // share ensures restApi.get() doesn't get called twice when the partitions are used below
+        .partition((data: DSpaceRESTV2Response) => Array.isArray(data._embedded));
+
+      return Observable.merge(
+
+        ifArray.map((data: DSpaceRESTV2Response) => {
+          return new DSpaceRESTv2Serializer(entry.request.resourceType).deserializeArray(data);
+        }).do((cos: CacheableObject[]) => cos.forEach((t) => this.addToObjectCache(t)))
+          .map((cos: Array<CacheableObject>): Array<string> => cos.map(t => t.uuid)),
+
+        ifNotArray.map((data: DSpaceRESTV2Response) => {
+          return new DSpaceRESTv2Serializer(entry.request.resourceType).deserialize(data);
+        }).do((co: CacheableObject) => this.addToObjectCache(co))
+          .map((co: CacheableObject): Array<string> => [co.uuid])
+
+      ).map((ids: Array<string>) => new SuccessResponse(ids))
+        .do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
+        .map((response: Response) => new RequestCompleteAction(entry.request.href))
+        .catch((error: Error) => Observable.of(new ErrorResponse(error.message))
+          .do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
+          .map((response: Response) => new RequestCompleteAction(entry.request.href)));
+    });
+
+  protected addToObjectCache(co: CacheableObject): void {
+    if (hasNoValue(co) || hasNoValue(co.uuid)) {
+      throw new Error('The server returned an invalid object');
+    }
+    this.objectCache.add(co, this.EnvConfig.cache.msToLive);
+  }
+}
diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9171bbe509811c0cfa80d646798e2c17198afc8a
--- /dev/null
+++ b/src/app/core/data/request.models.ts
@@ -0,0 +1,32 @@
+import { SortOptions } from "../cache/models/sort-options.model";
+import { PaginationOptions } from "../cache/models/pagination-options.model";
+import { GenericConstructor } from "../shared/generic-constructor";
+
+export class Request<T> {
+  constructor(
+    public href: string,
+    public resourceType: GenericConstructor<T>
+  ) {}
+}
+
+export class FindByIDRequest<T> extends Request<T> {
+  constructor(
+    href: string,
+    resourceType: GenericConstructor<T>,
+    public resourceID: string
+  ) {
+    super(href, resourceType);
+  }
+}
+
+export class FindAllRequest<T> extends Request<T> {
+  constructor(
+    href: string,
+    resourceType: GenericConstructor<T>,
+    public scopeID?: string,
+    public paginationOptions?: PaginationOptions,
+    public sortOptions?: SortOptions
+  ) {
+    super(href, resourceType);
+  }
+}
diff --git a/src/app/core/data/request.reducer.ts b/src/app/core/data/request.reducer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e20accc8317d11851f217c00474c8c7b4ac4854d
--- /dev/null
+++ b/src/app/core/data/request.reducer.ts
@@ -0,0 +1,81 @@
+import { CacheableObject } from "../cache/object-cache.reducer";
+import {
+  RequestActionTypes, RequestAction, RequestConfigureAction,
+  RequestExecuteAction, RequestCompleteAction
+} from "./request.actions";
+import { Request } from "./request.models";
+
+export class RequestEntry {
+  request: Request<CacheableObject>;
+  requestPending: boolean;
+  responsePending: boolean;
+  completed: boolean;
+}
+
+
+export interface RequestState {
+  [key: string]: RequestEntry
+}
+
+// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
+const initialState = Object.create(null);
+
+export const requestReducer = (state = initialState, action: RequestAction): RequestState => {
+  switch (action.type) {
+
+    case RequestActionTypes.CONFIGURE: {
+      return configureRequest(state, <RequestConfigureAction> action);
+    }
+
+    case RequestActionTypes.EXECUTE: {
+      return executeRequest(state, <RequestExecuteAction> action);
+    }
+
+    case RequestActionTypes.COMPLETE: {
+      return completeRequest(state, <RequestCompleteAction> action);
+    }
+
+    default: {
+      return state;
+    }
+  }
+};
+
+function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState {
+  return Object.assign({}, state, {
+    [action.payload.href]: {
+      request: action.payload,
+      requestPending: true,
+      responsePending: false,
+      completed: false
+    }
+  });
+}
+
+function executeRequest(state: RequestState, action: RequestExecuteAction): RequestState {
+  return Object.assign({}, state, {
+    [action.payload]: Object.assign({}, state[action.payload], {
+      requestPending: false,
+      responsePending: true
+    })
+  });
+}
+
+/**
+ * Update a request with the response
+ *
+ * @param state
+ *    the current state
+ * @param action
+ *    a RequestCompleteAction
+ * @return RequestState
+ *    the new state, with the response added to the request
+ */
+function completeRequest(state: RequestState, action: RequestCompleteAction): RequestState {
+  return Object.assign({}, state, {
+    [action.payload]: Object.assign({}, state[action.payload], {
+      responsePending: false,
+      completed: true
+    })
+  });
+}
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3b28af2c236c101197a09c1a6ef2ca1dbc4a35a
--- /dev/null
+++ b/src/app/core/data/request.service.ts
@@ -0,0 +1,48 @@
+import { Injectable } from "@angular/core";
+import { RequestEntry, RequestState } from "./request.reducer";
+import { Store } from "@ngrx/store";
+import { Request } from "./request.models";
+import { hasValue } from "../../shared/empty.util";
+import { Observable } from "rxjs/Observable";
+import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
+import { ResponseCacheService } from "../cache/response-cache.service";
+import { ObjectCacheService } from "../cache/object-cache.service";
+import { CacheableObject } from "../cache/object-cache.reducer";
+import { GenericConstructor } from "../shared/generic-constructor";
+
+@Injectable()
+export class RequestService {
+
+  constructor(
+    private objectCache: ObjectCacheService,
+    private responseCache: ResponseCacheService,
+    private store: Store<RequestState>
+  ) {
+  }
+
+  isPending(href: string): boolean {
+    let isPending = false;
+    this.store.select<RequestEntry>('core', 'data', 'request', href)
+      .take(1)
+      .subscribe((re: RequestEntry) =>  {
+        isPending = (hasValue(re) && !re.completed)
+    });
+
+    return isPending;
+  }
+
+  get(href: string): Observable<RequestEntry> {
+    return this.store.select<RequestEntry>('core', 'data', 'request', href);
+  }
+
+  configure<T extends CacheableObject>(href: string, normalizedType: GenericConstructor<T>): void {
+    const isCached = this.objectCache.hasBySelfLink(href);
+    const isPending = this.isPending(href);
+
+    if (!(isCached || isPending)) {
+      const request = new Request(href, normalizedType);
+      this.store.dispatch(new RequestConfigureAction(request));
+      this.store.dispatch(new RequestExecuteAction(href));
+    }
+  }
+}
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts
index 2661b3708dce4fd70c4784150249c4889815fd10..236244873cf9a288a10f0fbedcec275e12aac00d 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts
@@ -140,19 +140,20 @@ describe("DSpaceRESTv2Serializer", () => {
 
   describe("deserializeArray", () => {
 
-    it("should turn a valid document describing a collection of objects in to an array of valid models", () => {
-      const serializer = new DSpaceRESTv2Serializer(TestModel);
-      const doc = {
-        "_embedded": testResponses
-      };
-
-      const models = serializer.deserializeArray(doc);
-
-      expect(models[0].id).toBe(doc._embedded[0].id);
-      expect(models[0].name).toBe(doc._embedded[0].name);
-      expect(models[1].id).toBe(doc._embedded[1].id);
-      expect(models[1].name).toBe(doc._embedded[1].name);
-    });
+    //TODO rewrite to incorporate normalisation.
+    // it("should turn a valid document describing a collection of objects in to an array of valid models", () => {
+    //   const serializer = new DSpaceRESTv2Serializer(TestModel);
+    //   const doc = {
+    //     "_embedded": testResponses
+    //   };
+    //
+    //   const models = serializer.deserializeArray(doc);
+    //
+    //   expect(models[0].id).toBe(doc._embedded[0].id);
+    //   expect(models[0].name).toBe(doc._embedded[0].name);
+    //   expect(models[1].id).toBe(doc._embedded[1].id);
+    //   expect(models[1].name).toBe(doc._embedded[1].name);
+    // });
 
     //TODO cant implement/test this yet - depends on how relationships
     // will be handled in the rest api
diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts
index b5fa5983d8efbef770cca0bd56de25a60236677f..d4d5a7ce590b6bd6a537096137e7aad6061f20e6 100644
--- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts
+++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts
@@ -55,7 +55,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
     if (Array.isArray(response._embedded)) {
       throw new Error('Expected a single model, use deserializeArray() instead');
     }
-    return <T> Deserialize(response._embedded, this.modelType);
+    let normalized = Object.assign({}, response._embedded, this.normalizeLinks(response._embedded._links));
+    return <T> Deserialize(normalized, this.modelType);
   }
 
   /**
@@ -70,7 +71,26 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
     if (!Array.isArray(response._embedded)) {
       throw new Error('Expected an Array, use deserialize() instead');
     }
-    return <Array<T>> Deserialize(response._embedded, this.modelType);
+    let normalized = response._embedded.map((resource) => {
+       return Object.assign({}, resource, this.normalizeLinks(resource._links));
+    });
+
+    return <Array<T>> Deserialize(normalized, this.modelType);
+  }
+
+  private normalizeLinks(links:any): any {
+    let normalizedLinks = links;
+    for (let link in normalizedLinks) {
+      if (Array.isArray(normalizedLinks[link])) {
+        normalizedLinks[link] = normalizedLinks[link].map(linkedResource => {
+          return linkedResource.href;
+        });
+      }
+      else {
+        normalizedLinks[link] = normalizedLinks[link].href;
+      }
+    }
+    return normalizedLinks;
   }
 
 }
diff --git a/src/app/core/footer/footer.component.spec.ts b/src/app/core/footer/footer.component.spec.ts
index 1a4b26510bfd2c1286303460de5cd3ec07f19c4e..6015104003f1f551f0b9735ba659530767aba37d 100644
--- a/src/app/core/footer/footer.component.spec.ts
+++ b/src/app/core/footer/footer.component.spec.ts
@@ -10,7 +10,7 @@ import {
   DebugElement
 } from "@angular/core";
 import { By } from '@angular/platform-browser';
-import { TranslateModule, TranslateLoader } from "ng2-translate";
+import { TranslateModule, TranslateLoader } from "@ngx-translate/core";
 import { Store, StoreModule } from "@ngrx/store";
 
 // Load the implementations that should be tested
@@ -30,8 +30,10 @@ describe('Footer component', () => {
   beforeEach(async(() => {
     return TestBed.configureTestingModule({
       imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
-        provide: TranslateLoader,
-        useClass: MockTranslateLoader
+        loader: {
+          provide: TranslateLoader,
+          useClass: MockTranslateLoader
+        }
       })],
       declarations: [FooterComponent], // declare the test component
       providers: [
diff --git a/src/app/core/index/href-index.actions.ts b/src/app/core/index/href-index.actions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c00f2d96cb7798e8bd63682440cc193f2860a4e
--- /dev/null
+++ b/src/app/core/index/href-index.actions.ts
@@ -0,0 +1,58 @@
+import { Action } from "@ngrx/store";
+import { type } from "../../shared/ngrx/type";
+
+/**
+ * The list of HrefIndexAction type definitions
+ */
+export const HrefIndexActionTypes = {
+  ADD: type('dspace/core/index/href/ADD'),
+  REMOVE_UUID: type('dspace/core/index/href/REMOVE_UUID')
+};
+
+/**
+ * An ngrx action to add an href to the index
+ */
+export class AddToHrefIndexAction implements Action {
+  type = HrefIndexActionTypes.ADD;
+  payload: {
+    href: string;
+    uuid: string;
+  };
+
+  /**
+   * Create a new AddToHrefIndexAction
+   *
+   * @param href
+   *    the href to add
+   * @param uuid
+   *    the uuid of the resource the href links to
+   */
+  constructor(href: string, uuid: string) {
+    this.payload = { href, uuid };
+  }
+}
+
+/**
+ * An ngrx action to remove an href from the index
+ */
+export class RemoveUUIDFromHrefIndexAction implements Action {
+  type = HrefIndexActionTypes.REMOVE_UUID;
+  payload: string;
+
+  /**
+   * Create a new RemoveUUIDFromHrefIndexAction
+   *
+   * @param uuid
+   *    the uuid to remove all hrefs for
+   */
+  constructor(uuid: string) {
+    this.payload = uuid;
+  }
+}
+
+/**
+ * A type to encompass all HrefIndexActions
+ */
+export type HrefIndexAction
+  = AddToHrefIndexAction
+  | RemoveUUIDFromHrefIndexAction;
diff --git a/src/app/core/index/href-index.effects.ts b/src/app/core/index/href-index.effects.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e1c8ae8d1a7ce18c8e2cdb63b15d5a77f9c31e9
--- /dev/null
+++ b/src/app/core/index/href-index.effects.ts
@@ -0,0 +1,32 @@
+import { Injectable } from "@angular/core";
+import { Effect, Actions } from "@ngrx/effects";
+import {
+  ObjectCacheActionTypes, AddToObjectCacheAction,
+  RemoveFromObjectCacheAction
+} from "../cache/object-cache.actions";
+import { AddToHrefIndexAction, RemoveUUIDFromHrefIndexAction } from "./href-index.actions";
+import { hasValue } from "../../shared/empty.util";
+
+@Injectable()
+export class HrefIndexEffects {
+
+  constructor(
+    private actions$: Actions
+  ) { }
+
+  @Effect() add$ = this.actions$
+    .ofType(ObjectCacheActionTypes.ADD)
+    .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.self))
+    .map((action: AddToObjectCacheAction) => {
+      return new AddToHrefIndexAction(
+        action.payload.objectToCache.self,
+        action.payload.objectToCache.uuid
+      );
+    });
+
+  @Effect() remove$ = this.actions$
+    .ofType(ObjectCacheActionTypes.REMOVE)
+    .map((action: RemoveFromObjectCacheAction) => {
+      return new RemoveUUIDFromHrefIndexAction(action.payload);
+    });
+}
diff --git a/src/app/core/index/href-index.reducer.ts b/src/app/core/index/href-index.reducer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8cb46566df4d65eee08bb31b67691060b77f6ac8
--- /dev/null
+++ b/src/app/core/index/href-index.reducer.ts
@@ -0,0 +1,43 @@
+import {
+  HrefIndexAction, HrefIndexActionTypes, AddToHrefIndexAction,
+  RemoveUUIDFromHrefIndexAction
+} from "./href-index.actions";
+export interface HrefIndexState {
+  [href: string]: string
+}
+
+// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
+const initialState: HrefIndexState = Object.create(null);
+
+export const hrefIndexReducer = (state = initialState, action: HrefIndexAction): HrefIndexState => {
+  switch (action.type) {
+
+    case HrefIndexActionTypes.ADD: {
+      return addToHrefIndex(state, <AddToHrefIndexAction>action);
+    }
+
+    case HrefIndexActionTypes.REMOVE_UUID: {
+      return removeUUIDFromHrefIndex(state, <RemoveUUIDFromHrefIndexAction>action)
+    }
+
+    default: {
+      return state;
+    }
+  }
+};
+
+function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
+  return Object.assign({}, state, {
+    [action.payload.href]: action.payload.uuid
+  });
+}
+
+function removeUUIDFromHrefIndex(state: HrefIndexState, action: RemoveUUIDFromHrefIndexAction): HrefIndexState {
+  let newState = Object.create(null);
+  for (let href in state) {
+    if (state[href] !== action.payload) {
+      newState[href] = state[href];
+    }
+  }
+  return newState;
+}
diff --git a/src/app/core/index/index.reducers.ts b/src/app/core/index/index.reducers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e7e3d7218a2d87f70d7fe7a69aa95a06121d381d
--- /dev/null
+++ b/src/app/core/index/index.reducers.ts
@@ -0,0 +1,14 @@
+import { combineReducers } from "@ngrx/store";
+import { HrefIndexState, hrefIndexReducer } from "./href-index.reducer";
+
+export interface IndexState {
+  href: HrefIndexState
+}
+
+export const reducers = {
+  href: hrefIndexReducer
+};
+
+export function indexReducer(state: any, action: any) {
+  return combineReducers(reducers)(state, action);
+}
diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts
index 43217a1292fe1b1fb420021ac0700d8c77b83200..5325e395d830a4fc4bc1eb32b18fbfaf15f2b520 100644
--- a/src/app/core/shared/bitstream.model.ts
+++ b/src/app/core/shared/bitstream.model.ts
@@ -1,8 +1,7 @@
-import { inheritSerialization } from "cerialize";
 import { DSpaceObject } from "./dspace-object.model";
 import { Bundle } from "./bundle.model";
+import { RemoteData } from "../data/remote-data";
 
-@inheritSerialization(DSpaceObject)
 export class Bitstream extends DSpaceObject {
 
     /**
@@ -28,10 +27,16 @@ export class Bitstream extends DSpaceObject {
     /**
      * An array of Bundles that are direct parents of this Bitstream
      */
-    parents: Array<Bundle>;
+    parents: Array<RemoteData<Bundle>>;
 
     /**
      * The Bundle that owns this Bitstream
      */
     owner: Bundle;
+
+    /**
+     * The Bundle that owns this Bitstream
+     */
+    retrieve: string;
+
 }
diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts
index b990c8617e0022070523c0cde798b2407cc2c503..7c2f6b05d44df5bcba3bb69927031ed240a19117 100644
--- a/src/app/core/shared/bundle.model.ts
+++ b/src/app/core/shared/bundle.model.ts
@@ -1,23 +1,24 @@
-import { inheritSerialization } from "cerialize";
 import { DSpaceObject } from "./dspace-object.model";
 import { Bitstream } from "./bitstream.model";
 import { Item } from "./item.model";
+import { RemoteData } from "../data/remote-data";
 
-@inheritSerialization(DSpaceObject)
 export class Bundle extends DSpaceObject {
   /**
    * The primary bitstream of this Bundle
    */
-  primaryBitstream: Bitstream;
+  primaryBitstream: RemoteData<Bitstream>;
 
   /**
    * An array of Items that are direct parents of this Bundle
    */
-  parents: Array<Item>;
+  parents: Array<RemoteData<Item>>;
 
   /**
    * The Item that owns this Bundle
    */
   owner: Item;
 
+  bitstreams: Array<RemoteData<Bitstream>>
+
 }
diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts
index 7048ded4a4273b748553cbf3f330f20f5bb06570..4287eff63cdcb768a268e56a7f4289cb3adf02b2 100644
--- a/src/app/core/shared/collection.model.ts
+++ b/src/app/core/shared/collection.model.ts
@@ -1,14 +1,13 @@
-import { autoserialize, inheritSerialization } from "cerialize";
 import { DSpaceObject } from "./dspace-object.model";
 import { Bitstream } from "./bitstream.model";
+import { Item } from "./item.model";
+import { RemoteData } from "../data/remote-data";
 
-@inheritSerialization(DSpaceObject)
 export class Collection extends DSpaceObject {
 
   /**
    * A string representing the unique handle of this Collection
    */
-  @autoserialize
   handle: string;
 
   /**
@@ -54,16 +53,18 @@ export class Collection extends DSpaceObject {
   /**
    * The Bitstream that represents the logo of this Collection
    */
-  logo: Bitstream;
+  logo: RemoteData<Bitstream>;
 
   /**
    * An array of Collections that are direct parents of this Collection
    */
-  parents: Array<Collection>;
+  parents: Array<RemoteData<Collection>>;
 
   /**
    * The Collection that owns this Collection
    */
   owner: Collection;
 
+  items: Array<RemoteData<Item>>;
+
 }
diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9639abd258debc26ffad7ecbda6a7e53b9ead738
--- /dev/null
+++ b/src/app/core/shared/community.model.ts
@@ -0,0 +1,62 @@
+import { DSpaceObject } from "./dspace-object.model";
+import { Bitstream } from "./bitstream.model";
+import { Collection } from "./collection.model";
+import { RemoteData } from "../data/remote-data";
+
+export class Community extends DSpaceObject {
+
+  /**
+   * A string representing the unique handle of this Community
+   */
+  handle: string;
+
+  /**
+   * The introductory text of this Community
+   * Corresponds to the metadata field dc.description
+   */
+  get introductoryText(): string {
+    return this.findMetadata("dc.description");
+  }
+
+  /**
+   * The short description: HTML
+   * Corresponds to the metadata field dc.description.abstract
+   */
+  get shortDescription(): string {
+    return this.findMetadata("dc.description.abstract");
+  }
+
+  /**
+   * The copyright text of this Community
+   * Corresponds to the metadata field dc.rights
+   */
+  get copyrightText(): string {
+    return this.findMetadata("dc.rights");
+  }
+
+  /**
+   * The sidebar text of this Community
+   * Corresponds to the metadata field dc.description.tableofcontents
+   */
+  get sidebarText(): string {
+    return this.findMetadata("dc.description.tableofcontents");
+  }
+
+  /**
+   * The Bitstream that represents the logo of this Community
+   */
+  logo: RemoteData<Bitstream>;
+
+  /**
+   * An array of Communities that are direct parents of this Community
+   */
+  parents: Array<RemoteData<DSpaceObject>>;
+
+  /**
+   * The Community that owns this Community
+   */
+  owner: Community;
+
+  collections: Array<RemoteData<Collection>>;
+
+}
diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts
index 395886655f3090a9e17d8678954fd34a45629424..22769763bfa013a171793d733c1f2e55d883ccd4 100644
--- a/src/app/core/shared/dspace-object.model.ts
+++ b/src/app/core/shared/dspace-object.model.ts
@@ -2,73 +2,94 @@ import { autoserialize, autoserializeAs } from "cerialize";
 import { Metadatum } from "./metadatum.model"
 import { isEmpty, isNotEmpty } from "../../shared/empty.util";
 import { CacheableObject } from "../cache/object-cache.reducer";
+import { RemoteData } from "../data/remote-data";
 
 /**
  * An abstract model class for a DSpaceObject.
  */
 export abstract class DSpaceObject implements CacheableObject {
 
-  /**
-   * The human-readable identifier of this DSpaceObject
-   */
-  @autoserialize
-  id: string;
+    @autoserialize
+    self: string;
 
-  /**
-   * The universally unique identifier of this DSpaceObject
-   */
-  @autoserialize
-  uuid: string;
+    /**
+     * The human-readable identifier of this DSpaceObject
+     */
+    @autoserialize
+    id: string;
 
-  /**
-   * A string representing the kind of DSpaceObject, e.g. community, item, …
-   */
-  type: string;
+    /**
+     * The universally unique identifier of this DSpaceObject
+     */
+    @autoserialize
+    uuid: string;
 
-  /**
-   * The name for this DSpaceObject
-   */
-  @autoserialize
-  name: string;
+    /**
+     * A string representing the kind of DSpaceObject, e.g. community, item, …
+     */
+    type: string;
 
-  /**
-   * An array containing all metadata of this DSpaceObject
-   */
-  @autoserializeAs(Metadatum)
-  metadata: Array<Metadatum>;
+    /**
+     * The name for this DSpaceObject
+     */
+    @autoserialize
+    name: string;
 
-  /**
-   * An array of DSpaceObjects that are direct parents of this DSpaceObject
-   */
-  parents: Array<DSpaceObject>;
+    /**
+     * An array containing all metadata of this DSpaceObject
+     */
+    @autoserializeAs(Metadatum)
+    metadata: Array<Metadatum>;
 
-  /**
-   * The DSpaceObject that owns this DSpaceObject
-   */
-  owner: DSpaceObject;
+    /**
+     * An array of DSpaceObjects that are direct parents of this DSpaceObject
+     */
+    parents: Array<RemoteData<DSpaceObject>>;
 
-  /**
-   * Find a metadata field by key and language
-   *
-   * This method returns the value of the first element
-   * in the metadata array that matches the provided
-   * key and language
-   *
-   * @param key
-   * @param language
-   * @return string
-   */
-  findMetadata(key: string, language?: string): string {
-    const metadatum = this.metadata
-      .find((metadatum: Metadatum) => {
-        return metadatum.key === key &&
-          (isEmpty(language) || metadatum.language === language)
-      });
-    if (isNotEmpty(metadatum)) {
-      return metadatum.value;
+    /**
+     * The DSpaceObject that owns this DSpaceObject
+     */
+    owner: DSpaceObject;
+
+    /**
+     * Find a metadata field by key and language
+     *
+     * This method returns the value of the first element
+     * in the metadata array that matches the provided
+     * key and language
+     *
+     * @param key
+     * @param language
+     * @return string
+     */
+    findMetadata(key: string, language?: string): string {
+        const metadatum = this.metadata
+            .find((metadatum: Metadatum) => {
+                return metadatum.key === key &&
+                    (isEmpty(language) || metadatum.language === language)
+            });
+        if (isNotEmpty(metadatum)) {
+            return metadatum.value;
+        }
+        else {
+            return undefined;
+        }
     }
-    else {
-      return undefined;
+
+    /**
+     * Find metadata by an array of keys
+     *
+     * This method returns the values of the element
+     * in the metadata array that match the provided
+     * key(s)
+     *
+     * @param key(s)
+     * @return Array<Metadatum>
+     */
+    filterMetadata(keys: string[]): Array<Metadatum> {
+        return this.metadata
+            .filter((metadatum: Metadatum) => {
+                return keys.some(key => key === metadatum.key);
+            });
     }
-  }
 }
diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts
index 478d94f814e0b688bbb1a5cb81863083bf9badc6..92a05263a4f10d650bb878fceee6e78ceb3fbe0a 100644
--- a/src/app/core/shared/item.model.ts
+++ b/src/app/core/shared/item.model.ts
@@ -1,39 +1,80 @@
-import { inheritSerialization, autoserialize } from "cerialize";
 import { DSpaceObject } from "./dspace-object.model";
 import { Collection } from "./collection.model";
+import { RemoteData } from "../data/remote-data";
+import { Bundle } from "./bundle.model";
+import { Bitstream } from "./bitstream.model";
+import { Observable } from "rxjs";
 
-@inheritSerialization(DSpaceObject)
 export class Item extends DSpaceObject {
 
-  /**
-   * A string representing the unique handle of this Item
-   */
-  @autoserialize
-  handle: string;
-
-  /**
-   * The Date of the last modification of this Item
-   */
-  lastModified: Date;
-
-  /**
-   * A boolean representing if this Item is currently archived or not
-   */
-  isArchived: boolean;
-
-  /**
-   * A boolean representing if this Item is currently withdrawn or not
-   */
-  isWithdrawn: boolean;
-
-  /**
-   * An array of Collections that are direct parents of this Item
-   */
-  parents: Array<Collection>;
-
-  /**
-   * The Collection that owns this Item
-   */
-  owner: Collection;
+    /**
+     * A string representing the unique handle of this Item
+     */
+    handle: string;
+
+    /**
+     * The Date of the last modification of this Item
+     */
+    lastModified: Date;
+
+    /**
+     * A boolean representing if this Item is currently archived or not
+     */
+    isArchived: boolean;
+
+    /**
+     * A boolean representing if this Item is currently withdrawn or not
+     */
+    isWithdrawn: boolean;
+
+    /**
+     * An array of Collections that are direct parents of this Item
+     */
+    parents: Array<RemoteData<Collection>>;
+
+    /**
+     * The Collection that owns this Item
+     */
+    owner: Collection;
+
+    bundles: Array<RemoteData<Bundle>>;
+
+    getThumbnail(): Observable<Bitstream> {
+        const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
+        return bundle.flatMap(
+            bundle => {
+                if (bundle != null) {
+                    return bundle.primaryBitstream.payload;
+                }
+                else {
+                    return Observable.of(undefined);
+                }
+            }
+        );
+    }
+
+    getFiles(): Observable<Array<Observable<Bitstream>>> {
+        const bundle: Observable <Bundle> = this.getBundle("ORIGINAL");
+        return bundle.map(bundle => {
+            if (bundle != null) {
+                return bundle.bitstreams.map(bitstream => bitstream.payload)
+            }
+        });
+    }
+
+    getBundle(name: String): Observable<Bundle> {
+        return Observable.combineLatest(
+            ...this.bundles.map(b => b.payload),
+            (...bundles: Array<Bundle>) => bundles)
+            .map(bundles => {
+                return bundles.find((bundle: Bundle) => {
+                    return bundle.name === name
+                });
+            });
+    }
+
+    getCollections(): Array<Observable<Collection>> {
+        return this.parents.map(collection => collection.payload.map(parent => parent));
+    }
 
 }
diff --git a/src/app/core/shared/param-hash.spec.ts b/src/app/core/shared/param-hash.spec.ts
deleted file mode 100644
index f532c1523517fae8523c239fd9a3a9b570c18051..0000000000000000000000000000000000000000
--- a/src/app/core/shared/param-hash.spec.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { ParamHash } from "./param-hash";
-describe("ParamHash", () => {
-
-  it("should return a hash for a set of parameters", () => {
-    const hash = new ParamHash('azerty', true, 23).toString();
-
-    expect(hash).not.toBeNull();
-    expect(hash).not.toBe('');
-  });
-
-  it("should work with both simple and complex objects as parameters", () => {
-    const hash = new ParamHash('azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }).toString();
-
-    expect(hash).not.toBeNull();
-    expect(hash).not.toBe('');
-  });
-
-  it("should work with null or undefined as parameters", () => {
-    const hash1 = new ParamHash(undefined).toString();
-    const hash2 = new ParamHash(null).toString();
-    const hash3 = new ParamHash(undefined, null).toString();
-
-    expect(hash1).not.toBeNull();
-    expect(hash1).not.toBe('');
-    expect(hash2).not.toBeNull();
-    expect(hash2).not.toBe('');
-    expect(hash3).not.toBeNull();
-    expect(hash3).not.toBe('');
-    expect(hash1).not.toEqual(hash2);
-    expect(hash1).not.toEqual(hash3);
-    expect(hash2).not.toEqual(hash3);
-  });
-
-  it("should work if created without parameters", () => {
-    const hash1 = new ParamHash().toString();
-    const hash2 = new ParamHash().toString();
-
-    expect(hash1).not.toBeNull();
-    expect(hash1).not.toBe('');
-    expect(hash1).toEqual(hash2);
-  });
-
-  it("should create the same hash if created with the same set of parameters in the same order", () => {
-    const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
-    const hash1 = new ParamHash(...params).toString();
-    const hash2 = new ParamHash(...params).toString();
-
-    expect(hash1).toEqual(hash2);
-  });
-
-  it("should create a different hash if created with the same set of parameters in a different order", () => {
-    const params = ['azerty', true, 23, { "a": { "b": ['azerty', true] }, "c": 23 }];
-    const hash1 = new ParamHash(...params).toString();
-    const hash2 = new ParamHash(...params.reverse()).toString();
-
-    expect(hash1).not.toEqual(hash2);
-  });
-});
diff --git a/src/app/core/shared/param-hash.ts b/src/app/core/shared/param-hash.ts
deleted file mode 100644
index 9d07819ce512fa5f285561f57323a592375f2df6..0000000000000000000000000000000000000000
--- a/src/app/core/shared/param-hash.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Md5 } from "ts-md5/dist/md5";
-
-/**
- * Creates a hash of a set of parameters
- */
-export class ParamHash {
-  private params: Array<any>;
-
-  constructor(...params) {
-    this.params = params;
-  }
-
-  /**
-   * Returns an md5 hash based on the
-   * params passed to the constructor
-   *
-   * If you hash the same set of params in the
-   * same order the hashes will be identical
-   *
-   * @return {string}
-   *    an md5 hash
-   */
-  toString(): string {
-    let hash = new Md5();
-    this.params.forEach((param) => {
-      if (param === Object(param)) {
-        hash.appendStr(JSON.stringify(param));
-      }
-      else {
-        hash.appendStr('' + param);
-      }
-    });
-    return hash.end().toString();
-  }
-}
diff --git a/src/app/core/url-combiner/ui-url-combiner.ts b/src/app/core/url-combiner/ui-url-combiner.ts
index 260d33d1ca046341ed9a1fb66e2c1474b344bce8..c5254fdd41e52646c98fc710ca3704191110d7da 100644
--- a/src/app/core/url-combiner/ui-url-combiner.ts
+++ b/src/app/core/url-combiner/ui-url-combiner.ts
@@ -8,7 +8,7 @@ import { GlobalConfig } from "../../../config";
  * TODO write tests once GlobalConfig becomes injectable
  */
 export class UIURLCombiner extends URLCombiner{
-  constructor(...parts:Array<string>) {
-    super(GlobalConfig.ui.baseURL, GlobalConfig.ui.nameSpace, ...parts);
+  constructor(EnvConfig: GlobalConfig, ...parts: Array<string>) {
+    super(EnvConfig.ui.baseUrl, EnvConfig.ui.nameSpace, ...parts);
   }
 }
diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts
index 642111d87dd6def27e0eb9fd386da503326ff6de..0ed67763963c54527eb7f4b1af58bf53e77843cf 100644
--- a/src/app/header/header.component.spec.ts
+++ b/src/app/header/header.component.spec.ts
@@ -5,7 +5,7 @@ import { Store, StoreModule } from "@ngrx/store";
 import { HeaderState } from "./header.reducer";
 import Spy = jasmine.Spy;
 import { HeaderToggleAction } from "./header.actions";
-import { TranslateModule } from "ng2-translate";
+import { TranslateModule } from "@ngx-translate/core";
 import { NgbCollapseModule } from "@ng-bootstrap/ng-bootstrap";
 import { Observable } from "rxjs";
 
diff --git a/src/app/home/home-news/home-news.component.html b/src/app/home/home-news/home-news.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..4393228e94a22d295b254a409ccd298d1021c58c
--- /dev/null
+++ b/src/app/home/home-news/home-news.component.html
@@ -0,0 +1,19 @@
+<!--.row to offset the app component's .container-fluid padding-->
+<div class="row">
+  <div class="jumbotron jumbotron-fluid">
+    <div class="container-fluid">
+      <h1 class="display-3">Welcome to DSpace</h1>
+      <p class="lead">DSpace is an open source software platform that enables organisations to:</p>
+      <ul>
+        <li>capture and describe digital material using a submission workflow module, or a variety
+          of
+          programmatic ingest options
+        </li>
+        <li>distribute an organisation's digital assets over the web through a search and retrieval
+          system
+        </li>
+        <li>preserve digital assets over the long term</li>
+      </ul>
+    </div>
+  </div>
+</div>
diff --git a/src/app/home/home-news/home-news.component.scss b/src/app/home/home-news/home-news.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/app/home/home-news/home-news.component.ts b/src/app/home/home-news/home-news.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2cf3c9cf5660a6035dcc428514e236dfbc5ce1c9
--- /dev/null
+++ b/src/app/home/home-news/home-news.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit } from '@angular/core';
+
+@Component({
+  selector: 'ds-home-news',
+  styleUrls: ['./home-news.component.css'],
+  templateUrl: './home-news.component.html'
+})
+export class HomeNewsComponent implements OnInit {
+  constructor() {
+    this.universalInit();
+  }
+
+  universalInit() {
+
+  }
+
+  ngOnInit(): void {
+  }
+}
diff --git a/src/app/home/home.component.html b/src/app/home/home.component.html
index 06cac4108b8576fbc58329d172f9f88dfd78f8ea..fd7d4b6309832c6f3693369d0377892b7b52ceb6 100644
--- a/src/app/home/home.component.html
+++ b/src/app/home/home.component.html
@@ -1,3 +1,2 @@
-<div class="home">
-  Home component
-</div>
+<ds-home-news></ds-home-news>
+<ds-top-level-community-list></ds-top-level-community-list>
diff --git a/src/app/home/home.component.ts b/src/app/home/home.component.ts
index 9cb0704a0a4c9c13b7803e20c46fa61d46f72e28..9c46b797915236e557f0026a4c0615388d1b6a9d 100644
--- a/src/app/home/home.component.ts
+++ b/src/app/home/home.component.ts
@@ -1,16 +1,11 @@
-import { Component, ChangeDetectionStrategy, ViewEncapsulation } from '@angular/core';
+import { Component, OnInit } from '@angular/core';
 
 @Component({
-  changeDetection: ChangeDetectionStrategy.Default,
-  encapsulation: ViewEncapsulation.Emulated,
   selector: 'ds-home',
   styleUrls: ['./home.component.css'],
   templateUrl: './home.component.html'
 })
-export class HomeComponent {
-
-  data: any = {};
-
+export class HomeComponent implements OnInit {
   constructor() {
     this.universalInit();
   }
@@ -19,4 +14,6 @@ export class HomeComponent {
 
   }
 
+  ngOnInit(): void {
+  }
 }
diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts
index 5fb6a55b8db8a9cfe34e0d2511a04b12f5718986..cbb785c1c4a25a86d422516c24fecee912b48f20 100644
--- a/src/app/home/home.module.ts
+++ b/src/app/home/home.module.ts
@@ -2,13 +2,23 @@ import { NgModule } from '@angular/core';
 
 import { HomeComponent } from './home.component';
 import { HomeRoutingModule } from './home-routing.module';
+import { CommonModule } from "@angular/common";
+import { TopLevelCommunityListComponent } from "./top-level-community-list/top-level-community-list.component";
+import { HomeNewsComponent } from "./home-news/home-news.component";
+import { RouterModule } from "@angular/router";
+import { TranslateModule } from "@ngx-translate/core";
 
 @NgModule({
   imports: [
-    HomeRoutingModule
+    CommonModule,
+    HomeRoutingModule,
+    RouterModule,
+    TranslateModule
   ],
   declarations: [
-    HomeComponent
+    HomeComponent,
+    TopLevelCommunityListComponent,
+    HomeNewsComponent
   ]
 })
 export class HomeModule { }
diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.html b/src/app/home/top-level-community-list/top-level-community-list.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..7fe291ba8758a91e3a9fc36b9ff7d2c51d31d0a5
--- /dev/null
+++ b/src/app/home/top-level-community-list/top-level-community-list.component.html
@@ -0,0 +1,12 @@
+<div *ngIf="topLevelCommunities.hasSucceeded | async">
+  <h2>{{'home.top-level-communities.head' | translate}}</h2>
+  <p class="lead">{{'home.top-level-communities.help' | translate}}</p>
+  <ul>
+    <li *ngFor="let community of (topLevelCommunities.payload | async)">
+      <p>
+        <span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
+        <span class="text-muted">{{community.shortDescription}}</span>
+      </p>
+    </li>
+  </ul>
+</div>
diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.scss b/src/app/home/top-level-community-list/top-level-community-list.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.ts b/src/app/home/top-level-community-list/top-level-community-list.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87f9fad5179621a46f3b4b746ffd761e3d038b3d
--- /dev/null
+++ b/src/app/home/top-level-community-list/top-level-community-list.component.ts
@@ -0,0 +1,27 @@
+import { Component, OnInit } from '@angular/core';
+import { CommunityDataService } from "../../core/data/community-data.service";
+import { RemoteData } from "../../core/data/remote-data";
+import { Community } from "../../core/shared/community.model";
+
+@Component({
+  selector: 'ds-top-level-community-list',
+  styleUrls: ['./top-level-community-list.component.css'],
+  templateUrl: './top-level-community-list.component.html'
+})
+export class TopLevelCommunityListComponent implements OnInit {
+  topLevelCommunities: RemoteData<Community[]>;
+
+  constructor(
+    private cds: CommunityDataService
+  ) {
+    this.universalInit();
+  }
+
+  universalInit() {
+
+  }
+
+  ngOnInit(): void {
+    this.topLevelCommunities = this.cds.findAll();
+  }
+}
diff --git a/src/app/item-page/collections/collections.component.html b/src/app/item-page/collections/collections.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..bf764ae182e1ab85852a40f623f9644394e46cf5
--- /dev/null
+++ b/src/app/item-page/collections/collections.component.html
@@ -0,0 +1,7 @@
+<ds-metadata-field-wrapper [label]="label | translate">
+    <div class="collections">
+        <a *ngFor="let collection of collections; let last=last;" [href]="(collection | async)?.self">
+            <span>{{(collection | async)?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
+        </a>
+    </div>
+</ds-metadata-field-wrapper>
diff --git a/src/app/item-page/collections/collections.component.ts b/src/app/item-page/collections/collections.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..742ac247a13306204af98717edd5adf7072f4eb7
--- /dev/null
+++ b/src/app/item-page/collections/collections.component.ts
@@ -0,0 +1,34 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Collection } from "../../core/shared/collection.model";
+import { Observable } from "rxjs";
+import { Item } from "../../core/shared/item.model";
+
+@Component({
+    selector: 'ds-item-page-collections',
+    templateUrl: './collections.component.html'
+})
+export class CollectionsComponent implements OnInit {
+
+    @Input() item: Item;
+
+    label : string = "item.page.collections";
+
+    separator: string = "<br/>"
+
+    collections: Array<Observable<Collection>>;
+
+    constructor() {
+        this.universalInit();
+
+    }
+
+    universalInit() {
+    }
+
+    ngOnInit(): void {
+        this.collections = this.item.getCollections();
+    }
+
+
+
+}
diff --git a/src/app/item-page/file-section/file-section.component.html b/src/app/item-page/file-section/file-section.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..0cc15e090ec90a9f0ddb734ca01e0ba22052acf5
--- /dev/null
+++ b/src/app/item-page/file-section/file-section.component.html
@@ -0,0 +1,9 @@
+<ds-metadata-field-wrapper [label]="label | translate">
+    <div class="file-section">
+        <a *ngFor="let file of (files | async); let last=last;" [href]="(file | async)?.retrieve">
+            <span>{{(file | async)?.name}}</span>
+            <span>({{((file | async)?.size) | dsFileSize }})</span>
+            <span *ngIf="!last" innerHTML="{{separator}}"></span>
+        </a>
+    </div>
+</ds-metadata-field-wrapper>
diff --git a/src/app/item-page/file-section/file-section.component.ts b/src/app/item-page/file-section/file-section.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..08b01b8ea1ce124d056739abf3b7ae77df135261
--- /dev/null
+++ b/src/app/item-page/file-section/file-section.component.ts
@@ -0,0 +1,33 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Bitstream } from "../../core/shared/bitstream.model";
+import { Item } from "../../core/shared/item.model";
+import { Observable } from "rxjs";
+
+@Component({
+  selector: 'ds-item-page-file-section',
+  templateUrl: './file-section.component.html'
+})
+export class FileSectionComponent implements OnInit {
+
+  @Input() item: Item;
+
+  label : string = "item.page.files";
+
+  separator: string = "<br/>"
+
+  files: Observable<Array<Observable<Bitstream>>>;
+
+  constructor() {
+    this.universalInit();
+
+  }
+
+  universalInit() {
+  }
+
+  ngOnInit(): void {
+    this.files = this.item.getFiles();
+  }
+
+
+}
diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..64c0a607c55d153b646dfc95c28fb6cf0399388d
--- /dev/null
+++ b/src/app/item-page/item-page-routing.module.ts
@@ -0,0 +1,14 @@
+import { NgModule } from '@angular/core';
+import { RouterModule } from '@angular/router';
+
+import { ItemPageComponent } from './item-page.component';
+
+@NgModule({
+    imports: [
+        RouterModule.forChild([
+            { path: 'items/:id', pathMatch: 'full', component: ItemPageComponent },
+        ])
+    ]
+})
+export class ItemPageRoutingModule {
+}
diff --git a/src/app/item-page/item-page.component.html b/src/app/item-page/item-page.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..47e5330ca59af793ead4dc8ec837697fbbbaedc0
--- /dev/null
+++ b/src/app/item-page/item-page.component.html
@@ -0,0 +1,19 @@
+<div class="item-page" *ngIf="item.hasSucceeded | async">
+    <ds-item-page-title-field [item]="item.payload | async"></ds-item-page-title-field>
+    <div class="row">
+        <div class="col-xs-12 col-md-4">
+            <ds-metadata-field-wrapper>
+                <ds-thumbnail [thumbnail]="thumbnail | async"></ds-thumbnail>
+            </ds-metadata-field-wrapper>
+            <ds-item-page-file-section [item]="item.payload | async"></ds-item-page-file-section>
+            <ds-item-page-date-field [item]="item.payload | async"></ds-item-page-date-field>
+            <ds-item-page-author-field [item]="item.payload | async"></ds-item-page-author-field>
+        </div>
+        <div class="col-xs-12 col-md-6">
+            <ds-item-page-abstract-field
+                    [item]="item.payload | async"></ds-item-page-abstract-field>
+            <ds-item-page-uri-field [item]="item.payload | async"></ds-item-page-uri-field>
+            <ds-item-page-collections [item]="item.payload | async"></ds-item-page-collections>
+        </div>
+    </div>
+</div>
diff --git a/src/app/item-page/item-page.component.scss b/src/app/item-page/item-page.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..da97dd7a62e610066229abae8f4c43a981b82780
--- /dev/null
+++ b/src/app/item-page/item-page.component.scss
@@ -0,0 +1 @@
+@import '../../styles/variables.scss';
diff --git a/src/app/item-page/item-page.component.ts b/src/app/item-page/item-page.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c0dcafb3b04a142dde38a321d8e91f3f9aa5f1d4
--- /dev/null
+++ b/src/app/item-page/item-page.component.ts
@@ -0,0 +1,41 @@
+import { Component, OnInit } from '@angular/core';
+import { ActivatedRoute } from '@angular/router';
+import { Item } from "../core/shared/item.model";
+import { ItemDataService } from "../core/data/item-data.service";
+import { RemoteData } from "../core/data/remote-data";
+import { Observable } from "rxjs";
+import { Bitstream } from "../core/shared/bitstream.model";
+
+@Component({
+    selector: 'ds-item-page',
+    styleUrls: ['./item-page.component.css'],
+    templateUrl: './item-page.component.html',
+})
+export class ItemPageComponent implements OnInit {
+
+    id: number;
+
+    private sub: any;
+
+    item: RemoteData<Item>;
+
+    thumbnail: Observable<Bitstream>;
+
+    constructor(private route: ActivatedRoute, private items: ItemDataService) {
+        this.universalInit();
+    }
+
+    universalInit() {
+
+    }
+
+    ngOnInit(): void {
+        this.sub = this.route.params.subscribe(params => {
+            this.id = +params['id'];
+            this.item = this.items.findById(params['id']);
+            this.thumbnail = this.item.payload.flatMap(i => i.getThumbnail());
+        });
+    }
+
+
+}
diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ab0e2809f6467887570d262eb01ade2814e0ca52
--- /dev/null
+++ b/src/app/item-page/item-page.module.ts
@@ -0,0 +1,40 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { ItemPageComponent } from './item-page.component';
+import { ItemPageRoutingModule } from './item-page-routing.module';
+import { MetadataValuesComponent } from './metadata-values/metadata-values.component';
+import { MetadataUriValuesComponent } from './metadata-uri-values/metadata-uri-values.component';
+import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component';
+import { ItemPageAuthorFieldComponent } from './specific-field/author/item-page-author-field.component';
+import { ItemPageDateFieldComponent } from './specific-field/date/item-page-date-field.component';
+import { ItemPageAbstractFieldComponent } from './specific-field/abstract/item-page-abstract-field.component';
+import { ItemPageUriFieldComponent } from './specific-field/uri/item-page-uri-field.component';
+import { ItemPageTitleFieldComponent } from './specific-field/title/item-page-title-field.component';
+import { ItemPageSpecificFieldComponent } from './specific-field/item-page-specific-field.component';
+import { SharedModule } from './../shared/shared.module';
+import { FileSectionComponent } from "./file-section/file-section.component";
+import { CollectionsComponent } from "./collections/collections.component";
+
+@NgModule({
+    declarations: [
+        ItemPageComponent,
+        MetadataValuesComponent,
+        MetadataUriValuesComponent,
+        MetadataFieldWrapperComponent,
+        ItemPageAuthorFieldComponent,
+        ItemPageDateFieldComponent,
+        ItemPageAbstractFieldComponent,
+        ItemPageUriFieldComponent,
+        ItemPageTitleFieldComponent,
+        ItemPageSpecificFieldComponent,
+        FileSectionComponent,
+        CollectionsComponent
+    ],
+    imports: [
+        ItemPageRoutingModule,
+        CommonModule,
+        SharedModule
+    ]
+})
+export class ItemPageModule {
+}
diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..638501c8573e1357c836a4a37bd1c4cec1e7b760
--- /dev/null
+++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html
@@ -0,0 +1,6 @@
+<div class="simple-view-element">
+    <h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
+    <div class="simple-view-element-body">
+        <ng-content></ng-content>
+    </div>
+</div>
\ No newline at end of file
diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..749382bc9ac347c67ab1b4190df627cc954f3270
--- /dev/null
+++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss
@@ -0,0 +1,6 @@
+@import '../../../styles/variables.scss';
+:host {
+    .simple-view-element {
+        margin-bottom: 15px;
+    }
+}
\ No newline at end of file
diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..caae4bd5f18f29e691ca41649aa430d529f357ee
--- /dev/null
+++ b/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts
@@ -0,0 +1,21 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+  selector: 'ds-metadata-field-wrapper',
+  styleUrls: ['./metadata-field-wrapper.component.css'],
+  templateUrl: './metadata-field-wrapper.component.html'
+})
+export class MetadataFieldWrapperComponent {
+
+  @Input() label: string;
+
+  constructor() {
+    this.universalInit();
+
+  }
+
+  universalInit() {
+
+  }
+
+}
diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..cc618bcd501089c40dc33fb1a449cf5f751f0a84
--- /dev/null
+++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html
@@ -0,0 +1,5 @@
+<ds-metadata-field-wrapper [label]="label | translate">
+    <a *ngFor="let metadatum of values; let last=last;" [href]="metadatum.value">
+       {{ linktext || metadatum.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
+    </a>
+</ds-metadata-field-wrapper>
diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..50be6f5ad03dee5a13636a9571c7a2e4bf85181d
--- /dev/null
+++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss
@@ -0,0 +1 @@
+@import '../../../styles/variables.scss';
diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fa4a9ebfc571db56c2884ff94909acd25404f84e
--- /dev/null
+++ b/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts
@@ -0,0 +1,18 @@
+import { Component, Input } from '@angular/core';
+import { MetadataValuesComponent } from "../metadata-values/metadata-values.component";
+
+@Component({
+  selector: 'ds-metadata-uri-values',
+  styleUrls: ['./metadata-uri-values.component.css'],
+  templateUrl: './metadata-uri-values.component.html'
+})
+export class MetadataUriValuesComponent extends MetadataValuesComponent {
+
+  @Input() linktext: any;
+
+  @Input() values: any;
+
+  @Input() separator: string;
+
+  @Input() label: string;
+}
diff --git a/src/app/item-page/metadata-values/metadata-values.component.html b/src/app/item-page/metadata-values/metadata-values.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..f16655c63c7dfb7a10031a1288f6c01318bd79c5
--- /dev/null
+++ b/src/app/item-page/metadata-values/metadata-values.component.html
@@ -0,0 +1,5 @@
+<ds-metadata-field-wrapper [label]="label | translate">
+    <span *ngFor="let metadatum of values; let last=last;">
+        {{metadatum.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
+    </span>
+</ds-metadata-field-wrapper>
diff --git a/src/app/item-page/metadata-values/metadata-values.component.scss b/src/app/item-page/metadata-values/metadata-values.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..50be6f5ad03dee5a13636a9571c7a2e4bf85181d
--- /dev/null
+++ b/src/app/item-page/metadata-values/metadata-values.component.scss
@@ -0,0 +1 @@
+@import '../../../styles/variables.scss';
diff --git a/src/app/item-page/metadata-values/metadata-values.component.ts b/src/app/item-page/metadata-values/metadata-values.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6ac07ad914a385ce0030454dbd7e19ae33dee449
--- /dev/null
+++ b/src/app/item-page/metadata-values/metadata-values.component.ts
@@ -0,0 +1,25 @@
+import { Component, Input } from '@angular/core';
+
+@Component({
+  selector: 'ds-metadata-values',
+  styleUrls: ['./metadata-values.component.css'],
+  templateUrl: './metadata-values.component.html'
+})
+export class MetadataValuesComponent {
+
+  @Input() values: any;
+
+  @Input() separator: string;
+
+  @Input() label: string;
+
+  constructor() {
+    this.universalInit();
+
+  }
+
+  universalInit() {
+
+  }
+
+}
diff --git a/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3e88117654363af9c7cf5dd33d15184bcef051db
--- /dev/null
+++ b/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts
@@ -0,0 +1,21 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../../core/shared/item.model";
+import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component";
+
+@Component({
+    selector: 'ds-item-page-abstract-field',
+    templateUrl: './../item-page-specific-field.component.html'
+})
+export class ItemPageAbstractFieldComponent extends ItemPageSpecificFieldComponent {
+
+    @Input() item: Item;
+
+    separator : string;
+
+    fields : string[] = [
+        "dc.description.abstract"
+    ];
+
+    label : string = "item.page.abstract";
+
+}
diff --git a/src/app/item-page/specific-field/author/item-page-author-field.component.ts b/src/app/item-page/specific-field/author/item-page-author-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2ff76d509aea1b7b9f130e2faca2c5ca128fbede
--- /dev/null
+++ b/src/app/item-page/specific-field/author/item-page-author-field.component.ts
@@ -0,0 +1,23 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../../core/shared/item.model";
+import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component";
+
+@Component({
+  selector: 'ds-item-page-author-field',
+  templateUrl: './../item-page-specific-field.component.html'
+})
+export class ItemPageAuthorFieldComponent extends ItemPageSpecificFieldComponent {
+
+  @Input() item: Item;
+
+  separator : string;
+
+  fields : string[] = [
+    "dc.contributor.author",
+    "dc.creator",
+    "dc.contributor"
+  ];
+
+  label : string = "item.page.author";
+
+}
diff --git a/src/app/item-page/specific-field/date/item-page-date-field.component.ts b/src/app/item-page/specific-field/date/item-page-date-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..16b6aad1f0e47d7354f569d1ccbe2e506e053704
--- /dev/null
+++ b/src/app/item-page/specific-field/date/item-page-date-field.component.ts
@@ -0,0 +1,21 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../../core/shared/item.model";
+import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component";
+
+@Component({
+    selector: 'ds-item-page-date-field',
+    templateUrl: './../item-page-specific-field.component.html'
+})
+export class ItemPageDateFieldComponent extends ItemPageSpecificFieldComponent {
+
+    @Input() item: Item;
+
+    separator : string = ", ";
+
+    fields : string[] = [
+        "dc.date.issued"
+    ];
+
+    label : string = "item.page.date";
+
+}
diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.html b/src/app/item-page/specific-field/item-page-specific-field.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..4a27848ec66eef61597d822207762e4e15c611da
--- /dev/null
+++ b/src/app/item-page/specific-field/item-page-specific-field.component.html
@@ -0,0 +1,3 @@
+<div class="item-page-specific-field">
+  <ds-metadata-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
+</div>
diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.ts b/src/app/item-page/specific-field/item-page-specific-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4161505ccc838a4148858f82837d2b116b162fba
--- /dev/null
+++ b/src/app/item-page/specific-field/item-page-specific-field.component.ts
@@ -0,0 +1,24 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../core/shared/item.model";
+
+@Component({
+    templateUrl: './item-page-specific-field.component.html'
+})
+export class ItemPageSpecificFieldComponent {
+
+    @Input() item: Item;
+
+    fields : string[];
+
+    label : string;
+
+    separator : string = "<br/>";
+
+    constructor() {
+        this.universalInit();
+    }
+
+    universalInit() {
+
+    }
+}
diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.html b/src/app/item-page/specific-field/title/item-page-title-field.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..4c53e2e3e21d6955da0cd478d2f0cd1b2568355e
--- /dev/null
+++ b/src/app/item-page/specific-field/title/item-page-title-field.component.html
@@ -0,0 +1,3 @@
+<h2 class="item-page-title-field">
+  <ds-metadata-values [values]="item?.filterMetadata(fields)"></ds-metadata-values>
+</h2>
diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.ts b/src/app/item-page/specific-field/title/item-page-title-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6dc2d159c8d5bd97d348b68ffe3f29bc2eedb062
--- /dev/null
+++ b/src/app/item-page/specific-field/title/item-page-title-field.component.ts
@@ -0,0 +1,19 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../../core/shared/item.model";
+import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component";
+
+@Component({
+    selector: 'ds-item-page-title-field',
+    templateUrl: './item-page-title-field.component.html'
+})
+export class ItemPageTitleFieldComponent extends ItemPageSpecificFieldComponent {
+
+    @Input() item: Item;
+
+    separator : string;
+
+    fields : string[] = [
+        "dc.title"
+    ];
+
+}
diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.html b/src/app/item-page/specific-field/uri/item-page-uri-field.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..fde79d6a04a79e4e163026985582ba4514c1d85b
--- /dev/null
+++ b/src/app/item-page/specific-field/uri/item-page-uri-field.component.html
@@ -0,0 +1,3 @@
+<div class="item-page-specific-field">
+  <ds-metadata-uri-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
+</div>
diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts b/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4f7b0bb87449f6b3dab133b6a8703d5d88ed2b69
--- /dev/null
+++ b/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts
@@ -0,0 +1,21 @@
+import { Component, OnInit, Input } from '@angular/core';
+import { Item } from "../../../core/shared/item.model";
+import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component";
+
+@Component({
+  selector: 'ds-item-page-uri-field',
+  templateUrl: './item-page-uri-field.component.html'
+})
+export class ItemPageUriFieldComponent extends ItemPageSpecificFieldComponent {
+
+  @Input() item: Item;
+
+  separator : string;
+
+  fields : string[] = [
+    "dc.identifier.uri"
+  ];
+
+  label : string = "item.page.uri";
+
+}
diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts
index 0daca6a146043e4cd54fe62d4b08668b340a166f..93accef6e9383f3ca507e3d8d915cf91d1eb95c9 100644
--- a/src/app/shared/pagination/pagination.component.spec.ts
+++ b/src/app/shared/pagination/pagination.component.spec.ts
@@ -24,7 +24,7 @@ import { Ng2PaginationModule } from 'ng2-pagination';
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
 
 import { PaginationComponent } from './pagination.component';
-import { PaginationOptions } from '../../core/shared/pagination-options.model';
+import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
 import { MockTranslateLoader } from "../testing/mock-translate-loader";
 
 import { GLOBAL_CONFIG, EnvConfig } from '../../../config';
diff --git a/src/app/shared/pagination/pagination.component.ts b/src/app/shared/pagination/pagination.component.ts
index 20ff784a919070723a3b37004c44c970194bed6f..7705ff75dfb1896414e8e35e9bb525d450a48d90 100644
--- a/src/app/shared/pagination/pagination.component.ts
+++ b/src/app/shared/pagination/pagination.component.ts
@@ -15,7 +15,7 @@ import { Store } from "@ngrx/store";
 import { Observable } from "rxjs";
 
 import { HostWindowState } from "../host-window.reducer";
-import { PaginationOptions } from '../../core/shared/pagination-options.model';
+import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
 
 /**
  * The default pagination controls component.
diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts
index 91d590c86a1461368672a33e3362b3fd6a11d146..76f2c57ef923ab876ffd1292d2853a8cae1258a0 100644
--- a/src/app/shared/shared.module.ts
+++ b/src/app/shared/shared.module.ts
@@ -5,10 +5,13 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 
 import { Ng2PaginationModule } from 'ng2-pagination';
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
-import { TranslateModule } from 'ng2-translate/ng2-translate';
+import { TranslateModule } from '@ngx-translate/core';
 
 import { ApiService } from './api.service';
 import { PaginationComponent } from "./pagination/pagination.component";
+import { FileSizePipe } from "./utils/file-size-pipe";
+import { ThumbnailComponent } from "../thumbnail/thumbnail.component";
+import { SafeUrlPipe } from "./utils/safe-url-pipe";
 
 const MODULES = [
   // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -22,10 +25,13 @@ const MODULES = [
 ];
 
 const PIPES = [
+    FileSizePipe,
+    SafeUrlPipe
   // put pipes here
 ];
 
 const COMPONENTS = [
+  ThumbnailComponent
   // put shared components here
   PaginationComponent
 ];
diff --git a/src/app/shared/testing/mock-translate-loader.ts b/src/app/shared/testing/mock-translate-loader.ts
index a780766b259115fc9d0f8ba7c4059260e8f226b3..e739dcead3c0dbe0c2a8d4f5eddf0d7f244fbebe 100644
--- a/src/app/shared/testing/mock-translate-loader.ts
+++ b/src/app/shared/testing/mock-translate-loader.ts
@@ -1,4 +1,4 @@
-import { TranslateLoader } from "ng2-translate";
+import { TranslateLoader } from "@ngx-translate/core";
 import { Observable } from "rxjs";
 
 export class MockTranslateLoader implements TranslateLoader {
diff --git a/src/app/shared/utils/file-size-pipe.ts b/src/app/shared/utils/file-size-pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fbd3ae081cc538ca185b0a245770b33d32d846e4
--- /dev/null
+++ b/src/app/shared/utils/file-size-pipe.ts
@@ -0,0 +1,36 @@
+import { Pipe, PipeTransform } from '@angular/core';
+
+/*
+ * Convert bytes into largest possible unit.
+ * Takes an precision argument that defaults to 2.
+ * Usage:
+ *   bytes | fileSize:precision
+ * Example:
+ *   {{ 1024 |  fileSize}}
+ *   formats to: 1 KB
+ */
+@Pipe({name: 'dsFileSize'})
+export class FileSizePipe implements PipeTransform {
+
+    private units = [
+        'bytes',
+        'KiB',
+        'MiB',
+        'GiB',
+        'TiB',
+        'PiB'
+    ];
+
+    transform(bytes: number = 0, precision: number = 2 ) : string {
+        if ( isNaN( parseFloat( String(bytes) )) || ! isFinite( bytes ) ) return '?';
+
+        let unit = 0;
+
+        while ( bytes >= 1024 ) {
+            bytes /= 1024;
+            unit ++;
+        }
+
+        return bytes.toFixed( + precision ) + ' ' + this.units[ unit ];
+    }
+}
\ No newline at end of file
diff --git a/src/app/shared/utils/safe-url-pipe.ts b/src/app/shared/utils/safe-url-pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e05e58764be52c15df5782648b3ae3429e357c0c
--- /dev/null
+++ b/src/app/shared/utils/safe-url-pipe.ts
@@ -0,0 +1,10 @@
+import { Pipe, PipeTransform } from '@angular/core';
+import { DomSanitizer } from '@angular/platform-browser';
+
+@Pipe({name: 'dsSafeUrl'})
+export class SafeUrlPipe implements PipeTransform {
+    constructor(private domSanitizer: DomSanitizer) {}
+    transform(url) {
+        return this.domSanitizer.bypassSecurityTrustResourceUrl(url);
+    }
+}
\ No newline at end of file
diff --git a/src/app/thumbnail/thumbnail.component.html b/src/app/thumbnail/thumbnail.component.html
new file mode 100644
index 0000000000000000000000000000000000000000..757c1b8e4e983b2f4bcd815efbd3bedcc057026e
--- /dev/null
+++ b/src/app/thumbnail/thumbnail.component.html
@@ -0,0 +1,4 @@
+<div class="thumbnail">
+   <img *ngIf="thumbnail" [src]="thumbnail.retrieve"/>
+   <img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/>
+</div>
\ No newline at end of file
diff --git a/src/app/thumbnail/thumbnail.component.scss b/src/app/thumbnail/thumbnail.component.scss
new file mode 100644
index 0000000000000000000000000000000000000000..b14c7376e38df58fd2d59d62c11c2ecf6701ebe4
--- /dev/null
+++ b/src/app/thumbnail/thumbnail.component.scss
@@ -0,0 +1 @@
+@import '../../styles/variables.scss';
\ No newline at end of file
diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e8e226c004982be6a8ce191beeb22c259d7e98c6
--- /dev/null
+++ b/src/app/thumbnail/thumbnail.component.ts
@@ -0,0 +1,28 @@
+import { Component, Input, OnInit } from '@angular/core';
+import { Bitstream } from "../core/shared/bitstream.model";
+
+@Component({
+    selector: 'ds-thumbnail',
+    styleUrls: ['./thumbnail.component.css'],
+    templateUrl: './thumbnail.component.html'
+})
+export class ThumbnailComponent {
+
+    @Input() thumbnail: Bitstream;
+
+    data: any = {};
+
+    /**
+     * The default 'holder.js' image
+     */
+    holderSource: string = "data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23EEEEEE%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E";
+
+    constructor() {
+        this.universalInit();
+    }
+
+    universalInit() {
+
+    }
+
+}
diff --git a/src/backend/api.ts b/src/backend/api.ts
index bc62629737ed630de7771ea1c59172439b21fd8b..2fa94e9c286d165c90ce3f4d1ebdb887e3f0d9a8 100644
--- a/src/backend/api.ts
+++ b/src/backend/api.ts
@@ -1,3 +1,4 @@
+import { COMMUNITIES } from "./communities";
 const util = require('util');
 const { Router } = require('express');
 
@@ -51,6 +52,67 @@ export function createMockApi() {
 
   let router = Router();
 
+  router.route('/communities')
+    .get(function(req, res) {
+      console.log('GET');
+      // 70ms latency
+      setTimeout(function() {
+        res.json(toHALResponse(req, COMMUNITIES));
+      }, 0);
+
+    // })
+    // .post(function(req, res) {
+    //   console.log('POST', util.inspect(req.body, { colors: true }));
+    //   let community = req.body;
+    //   if (community) {
+    //     COMMUNITIES.push({
+    //       value: community.value,
+    //       created_at: new Date(),
+    //       completed: community.completed,
+    //       id: COMMUNITY_COUNT++
+    //     });
+    //     return res.json(community);
+    //   }
+    //
+    //   return res.end();
+    });
+
+  router.param('community_id', function(req, res, next, community_id) {
+    // ensure correct prop type
+    let id = req.params.community_id;
+    try {
+      req.community_id = id;
+      req.community = COMMUNITIES.find((community) => {
+        return community.id === id;
+      });
+      next();
+    } catch (e) {
+      next(new Error('failed to load community'));
+    }
+  });
+
+  router.route('/communities/:community_id')
+    .get(function(req, res) {
+      // console.log('GET', util.inspect(req.community.id, { colors: true }));
+      res.json(toHALResponse(req, req.community));
+    // })
+    // .put(function(req, res) {
+    //   console.log('PUT', util.inspect(req.body, { colors: true }));
+    //
+    //   let index = COMMUNITIES.indexOf(req.community);
+    //   let community = COMMUNITIES[index] = req.body;
+    //
+    //   res.json(community);
+    // })
+    // .delete(function(req, res) {
+    //   console.log('DELETE', req.community_id);
+    //
+    //   let index = COMMUNITIES.indexOf(req.community);
+    //   COMMUNITIES.splice(index, 1);
+    //
+    //   res.json(req.community);
+    });
+
   router.route('/collections')
     .get(function(req, res) {
       console.log('GET');
diff --git a/src/backend/bitstreams.ts b/src/backend/bitstreams.ts
index 480a0b4b55025ee7a4da054929e99ed5ee9d2595..ed47d0d94ad837e7acf59295f32785156a27ffb9 100644
--- a/src/backend/bitstreams.ts
+++ b/src/backend/bitstreams.ts
@@ -1,7 +1,7 @@
 export const BITSTREAMS = [
   {
     "_links": {
-      "self": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" },
+      "self": { "href": "/bitstreams/3678" },
       "bundle": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" },
       "retrieve": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa/retrieve" }
     },
@@ -22,7 +22,7 @@ export const BITSTREAMS = [
   },
   {
     "_links": {
-      "self": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" },
+      "self": { "href": "/bitstreams/8842" },
       "bundle": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" },
       "retrieve": { "href": "/rest/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632/retrieve" }
     },
diff --git a/src/backend/bundles.ts b/src/backend/bundles.ts
index 01e8f070027b753223b62ce1d79545d226dbec7c..9ec0630dbdebfc02f4b08cc4bd15e2dd71611b3b 100644
--- a/src/backend/bundles.ts
+++ b/src/backend/bundles.ts
@@ -1,14 +1,14 @@
 export const BUNDLES = [
   {
     "_links": {
-      "self": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" },
+      "self": { "href": "/bundles/2355" },
       "items": [
         { "href": "/items/8871" }
       ],
       "bitstreams": [
-        { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" },
+        { "href": "/bitstreams/3678" },
       ],
-      "primaryBitstream": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa" }
+      "primaryBitstream": { "href": "/bitstreams/3678" }
     },
     "id": "2355",
     "uuid": "35e0606d-5e18-4f9c-aa61-74fc751cc3f9",
@@ -19,14 +19,14 @@ export const BUNDLES = [
   },
   {
     "_links": {
-      "self": { "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd" },
+      "self": { "href": "/bundles/5687" },
       "items": [
         { "href": "/items/8871" }
       ],
       "bitstreams": [
-        { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" },
+        { "href": "/bitstreams/8842" },
       ],
-      "primaryBitstream": { "href": "/bitstreams/1a013ecc-fb25-4689-a44f-f1383ad26632" }
+      "primaryBitstream": { "href": "/bitstreams/8842" }
     },
     "id": "5687",
     "uuid": "a469c57a-abcf-45c3-83e4-b187ebd708fd",
diff --git a/src/backend/communities.ts b/src/backend/communities.ts
new file mode 100644
index 0000000000000000000000000000000000000000..940f6c72d57b64e40462770190331dc46fbf76f4
--- /dev/null
+++ b/src/backend/communities.ts
@@ -0,0 +1,86 @@
+export const COMMUNITIES = [
+  {
+    "name": "Community 1",
+    "handle": "10673/1",
+    "id": "6631",
+    "uuid": "83cd3281-f241-48be-9234-d876f8010d14",
+    "type": "community",
+    "metadata": [
+      {
+        "key": "dc.description",
+        "value": "<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\"http://www.duraspace.org/\">DuraSpace</a> logo).</p>",
+        "language": null
+      },
+      {
+        "key": "dc.description.abstract",
+        "value": "This is a sample top-level community",
+        "language": null
+      },
+      {
+        "key": "dc.description.tableofcontents",
+        "value": "<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>",
+        "language": null
+      },
+      {
+        "key": "dc.rights",
+        "value": "<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>",
+        "language": null
+      },
+      {
+        "key": "dc.title",
+        "value": "Sample Community",
+        "language": null
+      }
+    ],
+    "_links": {
+      "self": {
+        "href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863"
+      },
+      "collections": [
+        { "href": "/collections/5179" }
+      ]
+    }
+  },
+  {
+    "name": "Community 2",
+    "handle": "10673/2",
+    "id": "2365",
+    "uuid": "80eec4c6-70bd-4beb-b3d4-5d46c6343157",
+    "type": "community",
+    "metadata": [
+      {
+        "key": "dc.description",
+        "value": "<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\"http://www.duraspace.org/\">DuraSpace</a> logo).</p>",
+        "language": null
+      },
+      {
+        "key": "dc.description.abstract",
+        "value": "This is a sample top-level community",
+        "language": null
+      },
+      {
+        "key": "dc.description.tableofcontents",
+        "value": "<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>",
+        "language": null
+      },
+      {
+        "key": "dc.rights",
+        "value": "<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>",
+        "language": null
+      },
+      {
+        "key": "dc.title",
+        "value": "Sample Community",
+        "language": null
+      }
+    ],
+    "_links": {
+      "self": {
+        "href": "http://dspace7.4science.it/dspace-spring-rest/api/core/community/9076bd16-e69a-48d6-9e41-0238cb40d863"
+      },
+      "collections": [
+        { "href": "/collections/6547" }
+      ]
+    }
+  }
+];
diff --git a/src/backend/items.ts b/src/backend/items.ts
index 290e2b96aa2683bd9dec9df8783f06c86c1e71ea..dc155ff98c384d041514d8a196b8b4a05d587b83 100644
--- a/src/backend/items.ts
+++ b/src/backend/items.ts
@@ -4,7 +4,7 @@ export const ITEMS = [
       "self": {
         "href": "/items/8871"
       },
-      "collections": [
+      "parents": [
         {
           "href": "/collections/5179"
         },
@@ -14,11 +14,11 @@ export const ITEMS = [
       ],
       "bundles": [
         {
-          "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9"
+          "href": "/bundles/2355"
         },
-        {
-          "href": "/bundles/a469c57a-abcf-45c3-83e4-b187ebd708fd"
-        }
+        // {
+        //   "href": "/bundles/5687"
+        // }
       ]
     },
     "id": "8871",
@@ -96,7 +96,7 @@ export const ITEMS = [
       "self": {
         "href": "/items/9978"
       },
-      "collections": [
+      "parents": [
         {
           "href": "/collections/5179"
         },
@@ -106,11 +106,11 @@ export const ITEMS = [
       ],
       "bundles": [
         {
-          "href": "/bundles/b0176baa-d52e-4c20-a8e6-d586f2c70c76"
+          "href": "/bundles/2355"
         },
-        {
-          "href": "/bundles/40b1cd3f-07ad-4ca6-9716-132671f93a15"
-        }
+        // {
+        //   "href": "/bundles/5687"
+        // }
       ]
     },
     "id": "9978",
diff --git a/src/platform/modules/browser.module.ts b/src/platform/modules/browser.module.ts
index b3d809e852378deff77a13fd863462e997668d16..7f4081788f553f172add7fb3dfde5a69bc2a1a17 100755
--- a/src/platform/modules/browser.module.ts
+++ b/src/platform/modules/browser.module.ts
@@ -6,7 +6,8 @@ import { UniversalModule, isBrowser, isNode } from 'angular2-universal/browser';
 import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
 
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
-import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateHttpLoader } from "@ngx-translate/http-loader";
 
 import { AppModule, AppComponent } from '../../app/app.module';
 import { SharedModule } from '../../app/shared/shared.module';
@@ -27,8 +28,9 @@ import { GLOBAL_CONFIG, GlobalConfig, EnvConfig } from '../../config';
 
 // import * as LRU from 'modern-lru';
 
-export function createTranslateLoader(http: Http) {
-  return new TranslateStaticLoader(http, './assets/i18n', '.json');
+// AoT requires an exported function for factories
+export function HttpLoaderFactory(http: Http) {
+  return new TranslateHttpLoader(http);
 }
 
 export function getLRU(lru?: any) {
@@ -51,9 +53,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
   bootstrap: [AppComponent],
   imports: [
     TranslateModule.forRoot({
-      provide: TranslateLoader,
-      useFactory: (createTranslateLoader),
-      deps: [Http]
+      loader: {
+        provide: TranslateLoader,
+        useFactory: HttpLoaderFactory,
+        deps: [Http]
+      }
     }),
     NgbModule.forRoot(),
 
diff --git a/src/platform/modules/node.module.ts b/src/platform/modules/node.module.ts
index 7148855d4f3ce4d48e48b71f0e8886f31ad4993b..492e117521e2289b6584b3c139c154ee4f98be91 100755
--- a/src/platform/modules/node.module.ts
+++ b/src/platform/modules/node.module.ts
@@ -5,7 +5,8 @@ import { RouterModule } from '@angular/router';
 import { UniversalModule, isBrowser, isNode } from 'angular2-universal/node'; // for AoT we need to manually split universal packages
 
 import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
-import { TranslateLoader, TranslateModule, TranslateStaticLoader } from 'ng2-translate';
+import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
+import { TranslateHttpLoader } from "@ngx-translate/http-loader";
 
 import { AppModule, AppComponent } from '../../app/app.module';
 import { SharedModule } from '../../app/shared/shared.module';
@@ -13,7 +14,6 @@ import { CoreModule } from "../../app/core/core.module";
 
 import { StoreModule, Store } from "@ngrx/store";
 import { RouterStoreModule } from "@ngrx/router-store";
-import { StoreDevtoolsModule } from "@ngrx/store-devtools";
 import { rootReducer, AppState, NGRX_CACHE_KEY } from '../../app/app.reducers';
 import { effects } from '../../app/app.effects';
 
@@ -23,8 +23,9 @@ import { Meta } from '../angular2-meta';
 
 import { GLOBAL_CONFIG, EnvConfig } from '../../config';
 
-export function createTranslateLoader(http: Http) {
-  return new TranslateStaticLoader(http, './assets/i18n', '.json');
+// AoT requires an exported function for factories
+export function HttpLoaderFactory(http: Http) {
+  return new TranslateHttpLoader(http);
 }
 
 export function getLRU() {
@@ -43,9 +44,11 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
   bootstrap: [AppComponent],
   imports: [
     TranslateModule.forRoot({
-      provide: TranslateLoader,
-      useFactory: (createTranslateLoader),
-      deps: [Http]
+      loader: {
+        provide: TranslateLoader,
+        useFactory: HttpLoaderFactory,
+        deps: [Http]
+      }
     }),
     NgbModule.forRoot(),
 
@@ -59,7 +62,6 @@ export const UNIVERSAL_KEY = 'UNIVERSAL_CACHE';
     AppModule,
     StoreModule.provideStore(rootReducer),
     RouterStoreModule.connectRouter(),
-    StoreDevtoolsModule.instrumentOnlyWithExtension(),
     effects
   ],
   providers: [
diff --git a/src/server.aot.ts b/src/server.aot.ts
index de41c8cae8f2de03c2af27b04fc2be118734fb10..4d41fb973866775e0d3cf1a2281147c50cc9cc2c 100644
--- a/src/server.aot.ts
+++ b/src/server.aot.ts
@@ -124,5 +124,5 @@ app.get('*', function(req, res) {
 
 // Server
 let server = app.listen(app.get('port'), app.get('address'), () => {
-  console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`);
+  console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`);
 });
diff --git a/src/server.routes.ts b/src/server.routes.ts
index 02e79e563ac2d2506f0b9fb8ac3d2219e56cb469..704ea15df53456283474ad084db5862ab797eddf 100644
--- a/src/server.routes.ts
+++ b/src/server.routes.ts
@@ -10,5 +10,5 @@
  * ];
  **/
 export const routes: string[] = [
-  'home', '**'
+  'home', 'items/:id' , '**'
 ];
diff --git a/src/server.ts b/src/server.ts
index 9423add7a276e0ae20e901a313198c9a7928b48c..13837821d0c42be11c89a6f8e401ed2bbb2e9388 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -118,5 +118,5 @@ app.get('*', function(req, res) {
 
 // Server
 let server = app.listen(app.get('port'), app.get('address'), () => {
-  console.log(`Listening on: ${EnvConfig.ui.ssl ? 'https://' : 'http://'}://${server.address().address}:${server.address().port}`);
+  console.log(`[${new Date().toTimeString()}] Listening on ${EnvConfig.ui.ssl ? 'https://' : 'http://'}${server.address().address}:${server.address().port}`);
 });
diff --git a/src/typings.d.ts b/src/typings.d.ts
index 2e0285e9b451cb6ec2d3fe3257ce89ca6f2c89b1..be2597c60049907fde477c4a42d9b2ecbe3e7378 100644
--- a/src/typings.d.ts
+++ b/src/typings.d.ts
@@ -78,3 +78,5 @@ declare module "*.json" {
   const value: any;
   export default value;
 }
+
+declare module "reflect-metadata";
diff --git a/webpack.config.ts b/webpack.config.ts
index c51f1d2ce16f82f85b6b8da4363ada4e10cb392d..4da3f45b15f2fcb622023c90a5e47098cf1267d1 100644
--- a/webpack.config.ts
+++ b/webpack.config.ts
@@ -112,7 +112,7 @@ export var serverConfig = {
     ],
   },
   externals: includeClientPackages(
-    /@angularclass|@angular|angular2-|ng2-|ng-|@ng-|angular-|@ngrx|ngrx-|@angular2|ionic|@ionic|-angular2|-ng2|-ng/
+    /@angularclass|@angular|angular2-|ng2-|ng-|@ng-|angular-|@ngrx|ngrx-|@ngx-|@angular2|ionic|@ionic|-angular2|-ng2|-ng/
   ),
   node: {
     global: true,
diff --git a/yarn.lock b/yarn.lock
index eac42106d855aace5b3615d392b83c2c7f191fdd..97fa8f8cf64820c43dea67db2b531061c020249d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -113,6 +113,14 @@
     magic-string "^0.16.0"
     source-map "^0.5.6"
 
+"@ngx-translate/core@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/@ngx-translate/core/-/core-6.0.1.tgz#7c7a80077feb994fc815b67a72065af04d394efe"
+
+"@ngx-translate/http-loader@^0.0.3":
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-0.0.3.tgz#8346c8d2d6f630254601029668f17abe2afe8a9b"
+
 "@types/body-parser@0.0.33":
   version "0.0.33"
   resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-0.0.33.tgz#33ca1498fc37e51c5df0c81cae34569e7041e025"
@@ -3656,10 +3664,6 @@ nested-error-stacks@^1.0.0:
   dependencies:
     inherits "~2.0.1"
 
-ng2-translate@4.2.0:
-  version "4.2.0"
-  resolved "https://registry.yarnpkg.com/ng2-translate/-/ng2-translate-4.2.0.tgz#83bc8feca329b5fc56a636e36073241c6280c659"
-
 ngrx-store-freeze@^0.1.9:
   version "0.1.9"
   resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.1.9.tgz#b20f18f21fd5efc4e1b1e05f6f279674d0f70c81"
@@ -4717,9 +4721,9 @@ reflect-metadata@0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.2.tgz#ea23e5823dc830f292822bd3da9b89fd57bffb03"
 
-reflect-metadata@0.1.8, reflect-metadata@^0.1.2:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.8.tgz#72426d570b60776e3688968bd5ab9537a15cecf6"
+reflect-metadata@^0.1.10, reflect-metadata@^0.1.2:
+  version "0.1.10"
+  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.10.tgz#b4f83704416acad89988c9b15635d47e03b9344a"
 
 regenerate@^1.2.1:
   version "1.3.2"