diff --git a/core/authorize.php b/core/authorize.php
index d812f2e..08ed4f8 100644
--- a/core/authorize.php
+++ b/core/authorize.php
@@ -55,12 +55,13 @@ function authorize_access_allowed() {
 
 require_once __DIR__ . '/includes/bootstrap.inc';
 require_once __DIR__ . '/includes/common.inc';
+require_once __DIR__ . '/includes/database.inc';
 require_once __DIR__ . '/includes/file.inc';
 require_once __DIR__ . '/includes/module.inc';
 require_once __DIR__ . '/includes/ajax.inc';
 
 // Prepare a minimal bootstrap.
-drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 $request = \Drupal::request();
 
 // We have to enable the user and system modules, even to check access and
diff --git a/core/core.services.yml b/core/core.services.yml
index c158be4..2c7e896 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -64,6 +64,62 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [path]
+  page_cache:
+    class: Drupal\Core\PageCache\PageCacheInterface
+    factory_method: getPageCache
+    factory_service: page_cache_factory
+  page_cache_factory:
+    class: Drupal\Core\PageCache\DelegatePageCacheFactory
+    arguments: ['@page_cache_policy']
+    tags:
+      - { name: delegate_page_cache.factory }
+  page_cache_policy:
+    class: Drupal\Core\PageCache\CompoundPolicy
+    calls:
+      - [add, ['@page_cache_policy.deny_cli_or_unsafe_method']]
+  page_cache_policy.deny_cli_or_unsafe_method:
+    class: Drupal\Core\PageCache\PolicyRule\DenyCommandLineOrUnsafeMethod
+  page_cache_policy.deny_open_session:
+    class: Drupal\Core\PageCache\PolicyRule\DenyOpenSession
+  page_cache_policy.use_internal_config:
+    class: Drupal\Core\PageCache\PolicyRule\InternalCacheConfig
+    arguments: ['@config.factory']
+  page_cache_policy.cache_max_age_config:
+    class: Drupal\Core\PageCache\PolicyRule\MaxAgeConfig
+    arguments: ['@config.factory']
+  page_cache_selector.internal_storage:
+    class: Drupal\Core\PageCache\DelegatePageCache\PolicyBoundStorageSelector
+    arguments: ['@page_cache.internal_storage']
+    calls:
+      - [add, ['@page_cache_policy', '@page_cache_policy.use_internal_config']]
+      - [add, ['@page_cache_policy', '@page_cache_policy.deny_open_session']]
+    tags:
+      - { name: delegate_page_cache.storage_selector }
+  page_cache_selector.public_external_cache:
+    class: Drupal\Core\PageCache\DelegatePageCache\PolicyBoundCacheControlSelector
+    arguments: ['@page_cache.public_external_cache']
+    calls:
+      - [add, ['@page_cache_policy', '@page_cache_policy.cache_max_age_config']]
+      - [add, ['@page_cache_policy', '@page_cache_policy.deny_open_session']]
+    tags:
+      - { name: delegate_page_cache.cache_control_selector }
+  page_cache.cid_generator:
+    class: Drupal\Core\PageCache\Storage\CidGenerator
+    arguments: ['@content_negotiation']
+  page_cache_factory.internal_storage:
+    class: Drupal\Core\PageCache\Storage\InternalCacheFactory
+    arguments: ['@cache.page', '@page_cache.cid_generator', '@config.factory']
+  page_cache.internal_storage:
+    class: Drupal\Core\PageCache\StorageInterface
+    factory_method: getInternalCache
+    factory_service: page_cache_factory.internal_storage
+  page_cache_factory.cache_control:
+    class: Drupal\Core\PageCache\CacheControlDefaultFactory
+    arguments: ['@config.factory', '@settings']
+  page_cache.public_external_cache:
+    class: Drupal\Core\PageCache\CacheControlInterface
+    factory_method: getPublicExternal
+    factory_service: page_cache_factory.cache_control
   config.cachedstorage.storage:
     class: Drupal\Core\Config\FileStorage
     factory_class: Drupal\Core\Config\FileStorageFactory
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index cb75933..8b894d2 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -11,6 +11,7 @@
 use Drupal\Core\Database\Database;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\Extension\ExtensionDiscovery;
+use Drupal\Core\PageCache\PageCacheHelper;
 use Drupal\Core\Utility\Title;
 use Drupal\Core\Utility\Error;
 use Symfony\Component\ClassLoader\ApcClassLoader;
@@ -135,19 +136,14 @@
 const DRUPAL_BOOTSTRAP_KERNEL = 1;
 
 /**
- * Third bootstrap phase: try to serve a cached page.
- */
-const DRUPAL_BOOTSTRAP_PAGE_CACHE = 2;
-
-/**
  * Fourth bootstrap phase: load code for subsystems and modules.
  */
-const DRUPAL_BOOTSTRAP_CODE = 3;
+const DRUPAL_BOOTSTRAP_CODE = 2;
 
 /**
  * Final bootstrap phase: initialize language, path, theme, and modules.
  */
-const DRUPAL_BOOTSTRAP_FULL = 4;
+const DRUPAL_BOOTSTRAP_FULL = 3;
 
 /**
  * Role ID for anonymous users; should match what's in the "role" table.
@@ -710,62 +706,6 @@ function settings() {
 }
 
 /**
- * Gets the page cache cid for this request.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return string
- *   The cid for this request.
- */
-function drupal_page_cache_get_cid(Request $request) {
-  $cid_parts = array(
-    $request->getUri(),
-    \Drupal::service('content_negotiation')->getContentType($request),
-  );
-  return sha1(implode(':', $cid_parts));
-}
-
-/**
- * Retrieves the current page from the cache.
- *
- * Note: we do not serve cached pages to authenticated users, or to anonymous
- * users when $_SESSION is non-empty. $_SESSION may contain status messages
- * from a form submission, the contents of a shopping cart, or other user-
- * specific content that should not be cached and displayed to other users.
- *
- * @param \Symfony\Component\HttpFoundation\Request $request
- *   The request for this page.
- *
- * @return
- *   The cache object, if the page was found in the cache, NULL otherwise.
- */
-function drupal_page_get_cache(Request $request) {
-  if (drupal_page_is_cacheable()) {
-    return \Drupal::cache('page')->get(drupal_page_cache_get_cid($request));
-  }
-}
-
-/**
- * Determines the cacheability of the current page.
- *
- * @param $allow_caching
- *   Set to FALSE if you want to prevent this page to get cached.
- *
- * @return
- *   TRUE if the current page can be cached, FALSE otherwise.
- */
-function drupal_page_is_cacheable($allow_caching = NULL) {
-  $allow_caching_static = &drupal_static(__FUNCTION__, TRUE);
-  if (isset($allow_caching)) {
-    $allow_caching_static = $allow_caching;
-  }
-
-  return $allow_caching_static && ($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')
-    && !drupal_is_cli();
-}
-
-/**
  * Sets an HTTP response header for the current page.
  *
  * Note: When sending a Content-Type header, always include a 'charset' type,
@@ -937,106 +877,6 @@ function drupal_page_header() {
 }
 
 /**
- * Sets HTTP headers in preparation for a cached page response.
- *
- * The headers allow as much as possible in proxies and browsers without any
- * particular knowledge about the pages. Modules can override these headers
- * using drupal_add_http_header().
- *
- * If the request is conditional (using If-Modified-Since and If-None-Match),
- * and the conditions match those currently in the cache, a 304 Not Modified
- * response is sent.
- */
-function drupal_serve_page_from_cache(stdClass $cache, Response $response, Request $request) {
-  $config = \Drupal::config('system.performance');
-
-  // First half: we must determine if we should be returning a 304.
-
-  // Negotiate whether to use compression.
-  $page_compression = !empty($cache->data['page_compressed']) && extension_loaded('zlib');
-  $return_compressed = $page_compression && $request->server->has('HTTP_ACCEPT_ENCODING') && strpos($request->server->get('HTTP_ACCEPT_ENCODING'), 'gzip') !== FALSE;
-
-  // Get headers. Keys are lower-case.
-  $boot_headers = drupal_get_http_header();
-
-  foreach ($cache->data['headers'] as $name => $value) {
-    // In the case of a 304 response, certain headers must be sent, and the
-    // remaining may not (see RFC 2616, section 10.3.5).
-    $name_lower = strtolower($name);
-    if (in_array($name_lower, array('content-location', 'expires', 'cache-control', 'vary')) && !isset($boot_headers[$name_lower])) {
-      $response->headers->set($name, $value);
-      unset($cache->data['headers'][$name]);
-    }
-  }
-
-  // If the client sent a session cookie, a cached copy will only be served
-  // to that one particular client due to Vary: Cookie. Thus, do not set
-  // max-age > 0, allowing the page to be cached by external proxies, when a
-  // session cookie is present unless the Vary header has been replaced.
-  $max_age = !$request->cookies->has(session_name()) || isset($boot_headers['vary']) ? $config->get('cache.page.max_age') : 0;
-  $response->headers->set('Cache-Control', 'public, max-age=' . $max_age);
-
-  // Entity tag should change if the output changes.
-  $response->setEtag($cache->created);
-
-  // See if the client has provided the required HTTP headers.
-  $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
-  $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
-
-  if ($if_modified_since && $if_none_match
-      && $if_none_match == $response->headers->get('etag') // etag must match
-      && $if_modified_since == $cache->created) {  // if-modified-since must match
-    $response->setStatusCode(304);
-    return;
-  }
-
-  // Second half: we're not returning a 304, so put in other headers.
-
-  // Send the remaining headers.
-  foreach ($cache->data['headers'] as $name => $value) {
-    $response->headers->set($name, $value);
-    drupal_add_http_header($name, $value);
-  }
-
-  $response->setLastModified(\DateTime::createFromFormat('U', $cache->created));
-
-  // HTTP/1.0 proxies does not support the Vary header, so prevent any caching
-  // by sending an Expires date in the past. HTTP/1.1 clients ignores the
-  // Expires header if a Cache-Control: max-age= directive is specified (see RFC
-  // 2616, section 14.9.3).
-  if (!$response->getExpires()) {
-    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-  }
-
-  // Allow HTTP proxies to cache pages for anonymous users without a session
-  // cookie. The Vary header is used to indicates the set of request-header
-  // fields that fully determines whether a cache is permitted to use the
-  // response to reply to a subsequent request for a given URL without
-  // revalidation.
-  if (!isset($boot_headers['vary']) && !settings()->get('omit_vary_cookie')) {
-    $response->setVary('Cookie', FALSE);
-  }
-
-  if ($page_compression) {
-    $response->setVary('accept-encoding', FALSE);
-    // If page_compression is enabled, the cache contains gzipped data.
-    if ($return_compressed) {
-      // $cache->data['body'] is already gzip'ed, so make sure
-      // zlib.output_compression does not compress it once more.
-      ini_set('zlib.output_compression', '0');
-      $response->headers->set('content-encoding', 'gzip');
-    }
-    else {
-      // The client does not support compression, so unzip the data in the
-      // cache. Strip the gzip header and run uncompress.
-      $cache->data['body'] = gzinflate(substr(substr($cache->data['body'], 10), 0, -8));
-    }
-  }
-
-  $response->setContent($cache->data['body']);
-}
-
-/**
  * Translates a string to the current language or to a given language.
  *
  * The t() function serves two purposes. First, at run-time it translates
@@ -1347,7 +1187,7 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
     }
 
     // Mark this page as being uncacheable.
-    drupal_page_is_cacheable(FALSE);
+    \Drupal::service('page_cache')->setNotCacheable();
   }
 
   // Messages not set when DB connection fails.
@@ -1479,7 +1319,6 @@ function drupal_anonymous_user() {
  *   values:
  *   - DRUPAL_BOOTSTRAP_CONFIGURATION: Initializes configuration.
  *   - DRUPAL_BOOTSTRAP_KERNEL: Initalizes a kernel.
- *   - DRUPAL_BOOTSTRAP_PAGE_CACHE: Tries to serve a cached page.
  *   - DRUPAL_BOOTSTRAP_CODE: Loads code for subsystems and modules.
  *   - DRUPAL_BOOTSTRAP_FULL: Fully loads Drupal. Validates and fixes input
  *     data.
@@ -1492,7 +1331,6 @@ function drupal_bootstrap($phase = NULL) {
   static $phases = array(
     DRUPAL_BOOTSTRAP_CONFIGURATION,
     DRUPAL_BOOTSTRAP_KERNEL,
-    DRUPAL_BOOTSTRAP_PAGE_CACHE,
     DRUPAL_BOOTSTRAP_CODE,
     DRUPAL_BOOTSTRAP_FULL,
   );
@@ -1530,10 +1368,6 @@ function drupal_bootstrap($phase = NULL) {
           _drupal_bootstrap_kernel();
           break;
 
-        case DRUPAL_BOOTSTRAP_PAGE_CACHE:
-          _drupal_bootstrap_page_cache();
-          break;
-
         case DRUPAL_BOOTSTRAP_CODE:
           require_once __DIR__ . '/common.inc';
           _drupal_bootstrap_code();
@@ -1579,19 +1413,36 @@ function drupal_handle_request($test_only = FALSE) {
     exit;
   }
 
+  // Create a request object from the HttpFoundation.
+  $request = Request::createFromGlobals();
+
+  // Try to retrieve the page with an early page cache implementation configured
+  // using settings.
+  $page_cache_helper = new PageCacheHelper();
+  if ($page_cache_helper->deliverFromCacheBeforeKernel(settings(), $request)) {
+    return;
+  }
+
   $kernel = new DrupalKernel('prod', drupal_classloader(), !$test_only);
 
   // @todo Remove this once everything in the bootstrap has been
   //   converted to services in the DIC.
   $kernel->boot();
 
-  // Create a request object from the HttpFoundation.
-  $request = Request::createFromGlobals();
   \Drupal::getContainer()->set('request', $request);
 
+  // Try to retrieve the page from the cache.
+  if ($page_cache_helper->deliverFromCacheAfterContainer(\Drupal::service('page_cache'), $request)) {
+    return;
+  }
+
   drupal_bootstrap(DRUPAL_BOOTSTRAP_CODE);
 
-  $response = $kernel->handle($request)->prepare($request)->send();
+  $response = $kernel->handle($request);
+
+  // Save the rendered markup to the page cache and add relevant headers for
+  // external caches like browsers and proxies if necessary.
+  $page_cache_helper->recordAndDeliver(\Drupal::service('page_cache'), $response, $request);
 
   $kernel->terminate($request, $response);
 }
@@ -1742,49 +1593,6 @@ function _drupal_bootstrap_kernel() {
 }
 
 /**
- * Attempts to serve a page from the cache.
- */
-function _drupal_bootstrap_page_cache() {
-  global $user;
-
-  require_once __DIR__ . '/database.inc';
-  // Check for a cache mode force from settings.php.
-  if (settings()->get('page_cache_without_database')) {
-    $cache_enabled = TRUE;
-  }
-  else {
-    $config = \Drupal::config('system.performance');
-    $cache_enabled = $config->get('cache.page.use_internal');
-  }
-
-  $request = Request::createFromGlobals();
-  // If there is no session cookie and cache is enabled (or forced), try
-  // to serve a cached page.
-  if (!$request->cookies->has(session_name()) && $cache_enabled) {
-    // Make sure there is a user object because its timestamp will be checked.
-    $user = drupal_anonymous_user();
-    // Get the page from the cache.
-    $cache = drupal_page_get_cache($request);
-    // If there is a cached page, display it.
-    if (is_object($cache)) {
-      $response = new Response();
-      $response->headers->set('X-Drupal-Cache', 'HIT');
-      date_default_timezone_set(drupal_get_user_timezone());
-
-      drupal_serve_page_from_cache($cache, $response, $request);
-
-      // We are done.
-      $response->prepare($request);
-      $response->send();
-      exit;
-    }
-    else {
-      drupal_add_http_header('X-Drupal-Cache', 'MISS');
-    }
-  }
-}
-
-/**
  * Returns the current bootstrap phase for this Drupal process.
  *
  * The current phase is the one most recently completed by drupal_bootstrap().
diff --git a/core/includes/common.inc b/core/includes/common.inc
index 4e4409f..9f1f433 100644
--- a/core/includes/common.inc
+++ b/core/includes/common.inc
@@ -3158,6 +3158,7 @@ function drupal_valid_token($token, $value = '', $skip_anonymous = FALSE) {
  * Loads code for subsystems and modules, and registers stream wrappers.
  */
 function _drupal_bootstrap_code() {
+  require_once __DIR__ . '/database.inc';
   require_once __DIR__ . '/../../' . settings()->get('path_inc', 'core/includes/path.inc');
   require_once __DIR__ . '/module.inc';
   require_once __DIR__ . '/theme.inc';
@@ -3222,68 +3223,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
 }
 
 /**
- * Stores the current page in the cache.
- *
- * If page_compression is enabled, a gzipped version of the page is stored in
- * the cache to avoid compressing the output on each request. The cache entry
- * is unzipped in the relatively rare event that the page is requested by a
- * client without gzip support.
- *
- * Page compression requires the PHP zlib extension
- * (http://php.net/manual/ref.zlib.php).
- *
- * @param $body
- *   The response body.
- * @return
- *   The cached object or NULL if the page cache was not set.
- *
- * @see drupal_page_header()
- */
-function drupal_page_set_cache(Response $response, Request $request) {
-  if (drupal_page_is_cacheable()) {
-
-    // Check if the current page may be compressed.
-    $page_compressed = \Drupal::config('system.performance')->get('response.gzip') && extension_loaded('zlib');
-
-    $cache = (object) array(
-      'cid' => drupal_page_cache_get_cid($request),
-      'data' => array(
-        'body' => $response->getContent(),
-        'headers' => array(),
-        // We need to store whether page was compressed or not,
-        // because by the time it is read, the configuration might change.
-        'page_compressed' => $page_compressed,
-      ),
-      'tags' => array('content' => TRUE) + drupal_cache_tags_page_get($response),
-      'expire' => Cache::PERMANENT,
-      'created' => REQUEST_TIME,
-    );
-
-    $cache->data['headers'] = $response->headers->all();
-
-    // Hack: exclude the x-drupal-cache header; it may make it in here because
-    // of awkwardness in how we defer sending it over in _drupal_page_get_cache.
-    if (isset($cache->data['headers']['x-drupal-cache'])) {
-      unset($cache->data['headers']['x-drupal-cache']);
-    }
-
-     // Use the actual timestamp from an Expires header, if available.
-    if ($date = $response->getExpires()) {
-      $date = DrupalDateTime::createFromDateTime($date);
-      $cache->expire = $date->getTimestamp();
-    }
-
-    if ($cache->data['body']) {
-      if ($page_compressed) {
-        $cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
-      }
-      \Drupal::cache('page')->set($cache->cid, $cache->data, $cache->expire, $cache->tags);
-    }
-    return $cache;
-  }
-}
-
-/**
  * Sets the main page content value for later use.
  *
  * Given the nature of the Drupal page handling, this will be called once with
diff --git a/core/includes/session.inc b/core/includes/session.inc
index cb11801..b7a59a3 100644
--- a/core/includes/session.inc
+++ b/core/includes/session.inc
@@ -253,9 +253,6 @@ function drupal_session_initialize() {
     // anonymous users not use a session cookie unless something is stored in
     // $_SESSION. This allows HTTP proxies to cache anonymous pageviews.
     drupal_session_start();
-    if ($user->isAuthenticated() || !empty($_SESSION)) {
-      drupal_page_is_cacheable(FALSE);
-    }
   }
   else {
     // Set a session identifier for this request. This is necessary because
diff --git a/core/includes/utility.inc b/core/includes/utility.inc
index b18c941..f95ff3a 100644
--- a/core/includes/utility.inc
+++ b/core/includes/utility.inc
@@ -41,16 +41,12 @@ function drupal_rebuild() {
   restore_error_handler();
   restore_exception_handler();
 
-  // drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL) will build a new kernel. This
-  // comes before DRUPAL_BOOTSTRAP_PAGE_CACHE.
+  // drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL) will build a new kernel.
   PhpStorageFactory::get('service_container')->deleteAll();
   PhpStorageFactory::get('twig')->deleteAll();
 
-  // Disable the page cache.
-  drupal_page_is_cacheable(FALSE);
-
   // Bootstrap up to where caches exist and clear them.
-  drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+  drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
   foreach (Cache::getBins() as $bin) {
     $bin->deleteAll();
   }
diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php
index 8f4f66c..ff2cecb 100644
--- a/core/lib/Drupal/Core/CoreServiceProvider.php
+++ b/core/lib/Drupal/Core/CoreServiceProvider.php
@@ -8,6 +8,7 @@
 namespace Drupal\Core;
 
 use Drupal\Core\Cache\ListCacheBinsPass;
+use Drupal\Core\PageCache\DelegatePageCache\RegisterSelectorsPass;
 use Drupal\Core\DependencyInjection\ServiceProviderInterface;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\Compiler\ModifyServiceDefinitionsPass;
@@ -71,6 +72,7 @@ public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new RegisterPathProcessorsPass());
     $container->addCompilerPass(new RegisterRouteProcessorsPass());
     $container->addCompilerPass(new ListCacheBinsPass());
+    $container->addCompilerPass(new RegisterSelectorsPass());
     // Add the compiler pass for appending string translators.
     $container->addCompilerPass(new RegisterStringTranslatorsPass());
     // Add the compiler pass that will process the tagged breadcrumb builder
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 8e4596f..5e99544 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -7,8 +7,7 @@
 
 namespace Drupal\Core\EventSubscriber;
 
-use Drupal\Core\Language\Language;
-use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Language\LanguageManagerInterface;
 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
 use Symfony\Component\HttpKernel\KernelEvents;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -22,17 +21,17 @@ class FinishResponseSubscriber implements EventSubscriberInterface {
   /**
    * The LanguageManager object for retrieving the correct language code.
    *
-   * @var LanguageManager
+   * @var \Drupal\Core\Language\LanguageManagerInterface
    */
   protected $languageManager;
 
   /**
    * Constructs a FinishResponseSubscriber object.
    *
-   * @param LanguageManager $language_manager
-   *  The LanguageManager object for retrieving the correct language code.
+   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+   *   The LanguageManager object for retrieving the correct language code.
    */
-  public function __construct(LanguageManager $language_manager) {
+  public function __construct(LanguageManagerInterface $language_manager) {
     $this->languageManager = $language_manager;
   }
 
@@ -57,41 +56,6 @@ public function onRespond(FilterResponseEvent $event) {
     // Set the Content-language header.
     $response->headers->set('Content-language', $this->languageManager->getCurrentLanguage()->id);
 
-    // Because pages are highly dynamic, set the last-modified time to now
-    // since the page is in fact being regenerated right now.
-    // @todo Remove this and use a more intelligent default so that HTTP
-    // caching can function properly.
-    $response->setLastModified(new \DateTime(gmdate(DATE_RFC1123, REQUEST_TIME)));
-
-    // Also give each page a unique ETag. This will force clients to include
-    // both an If-Modified-Since header and an If-None-Match header when doing
-    // conditional requests for the page (required by RFC 2616, section 13.3.4),
-    // making the validation more robust. This is a workaround for a bug in
-    // Mozilla Firefox that is triggered when Drupal's caching is enabled and
-    // the user accesses Drupal via an HTTP proxy (see
-    // https://bugzilla.mozilla.org/show_bug.cgi?id=269303): When an
-    // authenticated user requests a page, and then logs out and requests the
-    // same page again, Firefox may send a conditional request based on the
-    // page that was cached locally when the user was logged in. If this page
-    // did not have an ETag header, the request only contains an
-    // If-Modified-Since header. The date will be recent, because with
-    // authenticated users the Last-Modified header always refers to the time
-    // of the request. If the user accesses Drupal via a proxy server, and the
-    // proxy already has a cached copy of the anonymous page with an older
-    // Last-Modified date, the proxy may respond with 304 Not Modified, making
-    // the client think that the anonymous and authenticated pageviews are
-    // identical.
-    // @todo Remove this line as no longer necessary per
-    //   http://drupal.org/node/1573064
-    $response->setEtag(REQUEST_TIME);
-
-    // Authenticated users are always given a 'no-cache' header, and will fetch
-    // a fresh page on every request. This prevents authenticated users from
-    // seeing locally cached pages.
-    // @todo Revisit whether or not this is still appropriate now that the
-    //   Response object does its own cache control processing and we intend to
-    //   use partial page caching more extensively.
-
     // Attach globally-declared headers to the response object so that Symfony
     // can send them for us correctly.
     // @todo remove this once we have removed all drupal_add_http_header() calls
@@ -99,15 +63,6 @@ public function onRespond(FilterResponseEvent $event) {
     foreach ($headers as $name => $value) {
       $response->headers->set($name, $value, FALSE);
     }
-
-    $max_age = \Drupal::config('system.performance')->get('cache.page.max_age');
-    if ($max_age > 0 && ($cache = drupal_page_set_cache($response, $request))) {
-      drupal_serve_page_from_cache($cache, $response, $request);
-    }
-    else {
-      $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
-      $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
-    }
   }
 
   /**
diff --git a/core/lib/Drupal/Core/PageCache/CacheControl/NoCache.php b/core/lib/Drupal/Core/PageCache/CacheControl/NoCache.php
new file mode 100644
index 0000000..a55949a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControl/NoCache.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControl\NoCache
+ */
+
+namespace Drupal\Core\PageCache\CacheControl;
+
+use Drupal\Core\PageCache\CacheControlBase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines a cache control generator which never allows caching.
+ */
+class NoCache extends CacheControlBase {
+  /**
+   * {@inheritdoc}
+   */
+  public function setCacheable(Response $response, Request $request) {
+    $this->setNotCacheable($response, $request);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControl/PrivateBrowser.php b/core/lib/Drupal/Core/PageCache/CacheControl/PrivateBrowser.php
new file mode 100644
index 0000000..ee13130
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControl/PrivateBrowser.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControl\PrivateBrowser.
+ */
+
+namespace Drupal\Core\PageCache\CacheControl;
+
+use Drupal\Core\PageCache\CacheControlBase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache control implementation for private browser cache.
+ */
+class PrivateBrowser extends CacheControlBase {
+  /**
+   * Number of seconds the response should be kept by the browser cache.
+   *
+   * @var int
+   */
+  protected $ttl;
+
+  /**
+   * Construct new backend controlling browser caches.
+   *
+   * @param int $ttl
+   *   Number of seconds the response should be kept by the browser cache.
+   */
+  public function __construct($ttl) {
+    $this->ttl = $ttl;
+  }
+
+  /**
+   * Enforce caching in browser.
+   *
+   * The Expires HTTP header is the heart of the client-side HTTP caching. The
+   * additional server-side page cache only takes effect when the client
+   * accesses the callback URL again (e.g., after clearing the browser cache or
+   * when force-reloading a Drupal page).
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  public function setCacheable(Response $response, Request $request) {
+    $response->setExpires(\DateTime::createFromFormat('U', REQUEST_TIME + $this->ttl));
+    $response->headers->set('Cache-Control', 'private');
+    $response->setMaxAge($this->ttl);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControl/PublicExternal.php b/core/lib/Drupal/Core/PageCache/CacheControl/PublicExternal.php
new file mode 100644
index 0000000..732434c
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControl/PublicExternal.php
@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControl\PublicExternal.
+ */
+
+namespace Drupal\Core\PageCache\CacheControl;
+
+use Drupal\Core\PageCache\CacheControlBase;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache-Control header generator for responses allowed to be cached by proxies.
+ */
+class PublicExternal extends CacheControlBase {
+  /**
+   * The number of seconds a cache may keep and deliver a response.
+   *
+   * @var int
+   */
+  protected $config;
+
+  /**
+   * Whether or not "Vary: Cookie" should be added to the response.
+   *
+   * @var bool
+   */
+  protected $omitVaryCookie;
+
+  /**
+   * Construct Cache-Control header generator publicly cacheable responses.
+   *
+   * @param int $max_age
+   *   The number of seconds a cache may keep and deliver a response.
+   * @param bool $omit_vary_cookie
+   *   Whether or not "Vary: Cookie" should be added to the response.
+   */
+  public function __construct($max_age, $omit_vary_cookie) {
+    $this->maxAge = (int) $max_age;
+    $this->omitVaryCookie = (bool) $omit_vary_cookie;
+  }
+
+  /**
+   * Sets HTTP headers in preparation for a cached page response.
+   *
+   * The headers allow caching as much as possible in proxies and browsers
+   * without any particular knowledge about the pages. Controllers and event
+   * listeners may supply additional headers by adding them directly to the
+   * response object.
+   *
+   * If the request is conditional (using If-Modified-Since and If-None-Match),
+   * and the conditions match those currently in the cache, a 304 Not Modified
+   * response is sent.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  public function setCacheable(Response $response, Request $request) {
+    // Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
+    if (!$response->headers->has('Expires')) {
+      $this->setExpiresNoCache($response);
+    }
+
+    // Check whether Vary was set to a custom value. Ignore Accept-Encoding,
+    // this has been set by the gzip filter.
+    //
+    // @todo: Let's get rid of that mechanism and instead instruct site builders
+    // and module authors to swap out page_cache.storage.external when custom
+    // Vary headers are required.
+    $vary_customized = (0 < count(array_diff($response->getVary(), array('Accept-Encoding'))));
+
+    // Replace the default restrictive cache-control header with a permissive
+    // one.
+    $response->headers->set('Cache-Control', 'public');
+    $max_age = !$request->cookies->has(session_name()) || $vary_customized ? $this->maxAge : 0;
+    $response->setMaxAge($max_age);
+
+    // Entity tag should change if the output changes.
+    if ($response->headers->has('Last-Modified')) {
+      $created = $response->getLastModified()->getTimestamp();
+    }
+    else {
+      $created = REQUEST_TIME;
+    }
+    $response->setEtag($created);
+
+    // Allow HTTP proxies to cache pages for anonymous users without a session
+    // cookie. The Vary header is used to indicates the set of request-header
+    // fields that fully determines whether a cache is permitted to use the
+    // response to reply to a subsequent request for a given URL without
+    // revalidation.
+    if (!$vary_customized && !$this->omitVaryCookie) {
+      $response->setVary('Cookie', FALSE);
+    }
+
+    // @todo: Replace this with Response::isNotModified()
+    $this->revalidateResponse($response, $request, $created);
+  }
+
+  /**
+   * Send an 304 Not Modified response if appropriate.
+   *
+   * If the request is conditional (using If-Modified-Since and If-None-Match),
+   * and the conditions match those currently in the cache, a 304 Not Modified
+   * response is sent.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object complete with headers and content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   */
+  protected function revalidateResponse(Response $response, Request $request, $created) {
+    // See if the client has provided the required HTTP headers.
+    $if_modified_since = $request->server->has('HTTP_IF_MODIFIED_SINCE') ? strtotime($request->server->get('HTTP_IF_MODIFIED_SINCE')) : FALSE;
+    $if_none_match = $request->server->has('HTTP_IF_NONE_MATCH') ? stripslashes($request->server->get('HTTP_IF_NONE_MATCH')) : FALSE;
+
+    if ($if_modified_since && $if_none_match
+      && $if_none_match == $response->headers->get('etag') // etag must match
+      && $if_modified_since == $created) {  // if-modified-since must match
+      $response->setStatusCode(304);
+      $response->setContent(null);
+
+      // In the case of a 304 response, certain headers must be sent, and the
+      // remaining may not (see RFC 2616, section 10.3.5).
+      foreach (array_keys($response->headers->all()) as $name) {
+        if (!in_array($name, array('content-location', 'expires', 'cache-control', 'vary'))) {
+          $response->headers->remove($name);
+        }
+      }
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControlBase.php b/core/lib/Drupal/Core/PageCache/CacheControlBase.php
new file mode 100644
index 0000000..cf5de23
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControlBase.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControlBase
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines an interface for Cache-Control header generators.
+ */
+abstract class CacheControlBase implements CacheControlInterface {
+  /**
+   * {@inheritdoc}
+   */
+  public function setNotCacheable(Response $response, Request $request) {
+    $this->setExpiresNoCache($response);
+    $this->setCacheControlNoCache($response);
+
+    // There is no point in sending along headers necessary for cache
+    // revalidation, if caching by proxies and browsers is denied in the first
+    // place. Therefore remove Etag and Last-Modified in that case.
+    $response->setEtag(NULL);
+    $response->setLastModified(NULL);
+    $response->setVary(NULL);
+  }
+
+  /**
+   * Disable caching in the browser and for HTTP/1.1 proxies and clients.
+   */
+  protected function setCacheControlNoCache(Response $response) {
+    $response->headers->set('Cache-Control', 'no-cache, must-revalidate, post-check=0, pre-check=0');
+  }
+
+  /**
+   * Disable caching in ancient browsers and for HTTP/1.0 proxies and clients.
+   *
+   * HTTP/1.0 proxies does not support the Vary header, so prevent any caching
+   * by sending an Expires date in the past. HTTP/1.1 clients ignores the
+   * Expires header if a Cache-Control: max-age= directive is specified (see
+   * RFC 2616, section 14.9.3).
+   */
+  protected function setExpiresNoCache(Response $response) {
+    $response->setExpires(\DateTime::createFromFormat('j-M-Y H:i:s T', '19-Nov-1978 05:00:00 GMT'));
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControlDefaultFactory.php b/core/lib/Drupal/Core/PageCache/CacheControlDefaultFactory.php
new file mode 100644
index 0000000..8b222e8
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControlDefaultFactory.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControlDefaultFactory
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Config\ConfigFactory;
+
+/**
+ * A cache control factory configured through config and settings.
+ */
+class CacheControlDefaultFactory {
+  /**
+   * Config object for system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
+   * Settings object.
+   *
+   * @var \Drupal\Component\Utility\Settings
+   */
+  protected $settings;
+
+  /**
+   * Construct a cache control factory.
+   *
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The configuration factory object.
+   * @param \Drupal\Component\Utility\Settings $settings
+   *   Settings object.
+   */
+  public function __construct(ConfigFactory $config_factory, Settings $settings) {
+    $this->config = $config_factory->get('system.performance');
+    $this->settings = $settings;
+  }
+
+  /**
+   * Return a PublicExternalCacheControl instance.
+   */
+  public function getPublicExternal() {
+    $max_age = $this->config->get('cache.page.max_age');
+    $omit_vary_cookie = $this->settings->get('omit_vary_cookie');
+    return new CacheControl\PublicExternal($max_age, $omit_vary_cookie);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CacheControlInterface.php b/core/lib/Drupal/Core/PageCache/CacheControlInterface.php
new file mode 100644
index 0000000..352692f
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CacheControlInterface.php
@@ -0,0 +1,36 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CacheControlInterface
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines an interface for Cache-Control header generators.
+ */
+interface CacheControlInterface {
+  /**
+   * Add Cache-Control and Expire header to a cacheable response.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  public function setCacheable(Response $response, Request $request);
+
+  /**
+   * Add Cache-Control and Expire header for a response which is not cacheable.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   */
+  public function setNotCacheable(Response $response, Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/CompoundPolicy.php b/core/lib/Drupal/Core/PageCache/CompoundPolicy.php
new file mode 100644
index 0000000..eb76e61
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CompoundPolicy.php
@@ -0,0 +1,93 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CompoundPolicy.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy implementation which can contain any number of rules.
+ */
+class CompoundPolicy implements PolicyRuleInterface {
+  /**
+   * A list of all policy rules which will be applied to the request.
+   *
+   * @var \Drupal\Core\PageCache\PolicyRuleInterface[]
+   */
+  protected $rules = array();
+
+  /**
+   * A list of policy rules which are required to pass.
+   *
+   * @var \Drupal\Core\PageCache\PolicyRuleInterface[]
+   */
+  protected $mandatoryRules = array();
+
+  /**
+   * A list of policy rule observers notified when results are available.
+   *
+   * @var \Drupal\Core\PageCache\CompoundPolicyObserverInterface[]
+   */
+  protected $observers = array();
+
+  /**
+   * A mapping of policy observers to policy rules.
+   *
+   * @var array
+   */
+  protected $observedRules = array();
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    // Fail when there are no rules at all.
+    if (empty($this->rules)) {
+      return FALSE;
+    }
+
+    // Apply all policy rules and collect the results.
+    $result = array();
+    foreach ($this->rules as $rule_hash => $rule) {
+      $result[$rule_hash] = $rule->apply($request);
+    }
+
+    // Notify observers.
+    foreach ($this->observers as $observer_hash => $observer) {
+      $partial_result = $this->checkResult($result, $this->observedRules[$observer_hash]);
+      $observer->onCompoundPolicyResult($this->observedRules[$observer_hash], $partial_result);
+    }
+
+    return $this->checkResult($result, $this->mandatoryRules);
+  }
+
+  /**
+   * Add a mapping between a rule and a observer.
+   */
+  public function add(PolicyRuleInterface $rule, $mandatory = TRUE, CompoundPolicyObserverInterface $observer = NULL) {
+    $rule_hash = spl_object_hash($rule);
+    $this->rules[$rule_hash] = $rule;
+
+    if ($mandatory) {
+      $this->mandatoryRules[$rule_hash] = $rule_hash;
+    }
+
+    if (isset($observer)) {
+      $observer_hash = spl_object_hash($observer);
+      $this->observers[$observer_hash] = $observer;
+      $this->observedRules[$observer_hash][$rule_hash] = $rule_hash;
+    }
+  }
+
+  /**
+   * Verify that a result array only consists of success-results.
+   */
+  protected function checkResult(array $result, array $rules) {
+    $rules_result = array_intersect_key($result, $rules);
+    return $rules_result == array_filter($rules_result);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/CompoundPolicyObserverInterface.php b/core/lib/Drupal/Core/PageCache/CompoundPolicyObserverInterface.php
new file mode 100644
index 0000000..3cb3b30
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/CompoundPolicyObserverInterface.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\CompoundPolicyObserverInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * Defines the interface for policy rule observers.
+ */
+interface CompoundPolicyObserverInterface {
+  /**
+   * Verify results produced by applying a set of policy rules.
+   *
+   * @param \Drupal\Core\PageCache\PolicyRuleInterface[] $rules
+   *   A list of policy rules applied.
+   * @param bool $result
+   *   TRUE if all of the rules applied successfully.
+   */
+  public function onCompoundPolicyResult(array $rules, $result);
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache.php
new file mode 100644
index 0000000..00fc757
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache.php
@@ -0,0 +1,81 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCache.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Page cache implementation relying on selectors for storage / cache-control.
+ */
+class DelegatePageCache extends PageCacheBase {
+  /**
+   * A list of storage selectors.
+   *
+   * @var \Drupal\Core\PageCache\DelegatePageCache\StorageSelectorInterface[]
+   */
+  protected $storageSelectors;
+
+  /**
+   * A list of cache control selectors.
+   *
+   * @var \Drupal\Core\PageCache\DelegatePageCache\CacheControlSelectorInterface[]
+   */
+  protected $cacheControlSelectors;
+
+  /**
+   * Constructs a new page cache service.
+   */
+  public function __construct(PolicyRuleInterface $policy, array $storage_selectors, array $cache_control_selectors) {
+    parent::__construct($policy);
+    $this->storageSelectors = $storage_selectors;
+    $this->cacheControlSelectors = $cache_control_selectors;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function prepareCacheGet(Request $request) {
+    parent::prepareCacheGet($request);
+
+    if ($this->isCacheable()) {
+      $this->selectStorage($request);
+      $this->selectCacheControl($request);
+    }
+  }
+
+  /**
+   * Evaluate all storage selectors and set storage if appropriate.
+   */
+  protected function selectStorage(Request $request) {
+    $this->storage = NULL;
+
+    foreach ($this->storageSelectors as $storage_selector) {
+      $storage = $storage_selector->selectStorage($request);
+      if ($storage) {
+        $this->setStorage($storage);
+        return;
+      }
+    }
+  }
+
+  /**
+   * Evaluate all cache control selectors and set cache control.
+   */
+  protected function selectCacheControl(Request $request) {
+    $this->cacheControl = NULL;
+
+    foreach ($this->cacheControlSelectors as $cache_control_selector) {
+      $cache_control = $cache_control_selector->selectCacheControl($request);
+      if ($cache_control) {
+        $this->setCacheControl($cache_control);
+        return;
+      }
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/CacheControlSelectorInterface.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/CacheControlSelectorInterface.php
new file mode 100644
index 0000000..ab7033d
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/CacheControlSelectorInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\CacheControlSelectorInterface.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for storage selector implementations.
+ */
+interface CacheControlSelectorInterface {
+  /**
+   * Return a cache control object suitable for the given request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return \Drupal\Core\PageCache\CacheControlInterface|NULL
+   *   An appropriate storage or NULL if none is available.
+   */
+  public function selectCacheControl(Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundCacheControlSelector.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundCacheControlSelector.php
new file mode 100644
index 0000000..492ac7e
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundCacheControlSelector.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\PolicyBoundCacheControlSelector.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Drupal\Core\PageCache\CacheControlInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for cache control selector implementations.
+ */
+class PolicyBoundCacheControlSelector extends PolicyBoundSelectorBase implements CacheControlSelectorInterface {
+  /**
+   * The cache control object selected when policies evaluate to TRUE.
+   *
+   * @var \Drual\Core\PageCache\CacheControlInterface
+   */
+  protected $cacheControl;
+
+  /**
+   * Construct new policy bound cache control selector.
+   *
+   * @param \Drual\Core\PageCache\CacheControlInterface $cache_control
+   *   The cache control object selected when policies evaluate to TRUE.
+   */
+  public function __construct(CacheControlInterface $cache_control) {
+    $this->cacheControl = $cache_control;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function selectCacheControl(Request $request) {
+    if ($this->policyResult) {
+      return $this->cacheControl;
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundSelectorBase.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundSelectorBase.php
new file mode 100644
index 0000000..b6b6882
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundSelectorBase.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\PolicyBoundSelectorBase.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Drupal\Core\PageCache\CompoundPolicy;
+use Drupal\Core\PageCache\CompoundPolicyObserverInterface;
+use Drupal\Core\PageCache\PolicyRuleInterface;
+
+/**
+ * A base class implementing common methods for policy bound selector classes.
+ */
+class PolicyBoundSelectorBase implements CompoundPolicyObserverInterface {
+  /**
+   * Flag indicating whether all policy rules applied successfully.
+   *
+   * @var bool
+   */
+  protected $policyResult = FALSE;
+
+  /**
+   * Add non-mandatory policy rule to the compound policy.
+   */
+  public function add(CompoundPolicy $policy, PolicyRuleInterface $rule) {
+    $policy->add($rule, FALSE, $this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function onCompoundPolicyResult(array $rules, $result) {
+    $this->policyResult = $result;
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundStorageSelector.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundStorageSelector.php
new file mode 100644
index 0000000..efe888f
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/PolicyBoundStorageSelector.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\PolicyBoundStorageSelector.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Drupal\Core\PageCache\StorageInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for storage selector implementations.
+ */
+class PolicyBoundStorageSelector extends PolicyBoundSelectorBase implements StorageSelectorInterface {
+  /**
+   * The storage object selected when policies evaluate to TRUE.
+   *
+   * @var \Drual\Core\PageCache\StorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Construct new policy bound storage selector.
+   *
+   * @param \Drual\Core\PageCache\StorageInterface $storage
+   *   The storage object selected when policies evaluate to TRUE.
+   */
+  public function __construct(StorageInterface $storage) {
+    $this->storage = $storage;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function selectStorage(Request $request) {
+    if ($this->policyResult) {
+      return $this->storage;
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/RegisterSelectorsPass.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/RegisterSelectorsPass.php
new file mode 100644
index 0000000..7fe3331
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/RegisterSelectorsPass.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\RegisterSelectorsPass.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Reference;
+
+
+/**
+ * Adds services tagged as page_cache.policy to the page_cache service.
+ */
+class RegisterSelectorsPass implements CompilerPassInterface {
+
+  /**
+   * Implements CompilerPassInterface::process().
+   */
+  public function process(ContainerBuilder $container) {
+    foreach ($container->findTaggedServiceIds('delegate_page_cache.factory') as $id => $attributes) {
+      $factorydef = $container->getDefinition($id);
+
+      // Register all services tagged with delegate_page_cache.storage_selector
+      // with the page cache service.
+      foreach ($container->findTaggedServiceIds('delegate_page_cache.storage_selector') as $id => $attributes) {
+        $def = $container->getDefinition($id);
+        $class = $def->getClass();
+        $reflection_class = new \ReflectionClass($class);
+        if (!$reflection_class->implementsInterface('Drupal\Core\PageCache\DelegatePageCache\StorageSelectorInterface')) {
+          throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
+        }
+
+        $factorydef->addMethodCall('addStorageSelector', array(new Reference($id)));
+      }
+
+      // Register all services tagged with delegate_page_cache.cache_control_selector
+      // with the page cache service.
+      foreach ($container->findTaggedServiceIds('delegate_page_cache.cache_control_selector') as $id => $attributes) {
+        $def = $container->getDefinition($id);
+        $class = $def->getClass();
+        $reflection_class = new \ReflectionClass($class);
+        if (!$reflection_class->implementsInterface('Drupal\Core\PageCache\DelegatePageCache\CacheControlSelectorInterface')) {
+          throw new \InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
+        }
+
+        $factorydef->addMethodCall('addCacheControlSelector', array(new Reference($id)));
+      }
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCache/StorageSelectorInterface.php b/core/lib/Drupal/Core/PageCache/DelegatePageCache/StorageSelectorInterface.php
new file mode 100644
index 0000000..cdbbbc3
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCache/StorageSelectorInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCache\StorageSelectorInterface.
+ */
+
+namespace Drupal\Core\PageCache\DelegatePageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for storage selector implementations.
+ */
+interface StorageSelectorInterface {
+  /**
+   * Return a storage object suitable for the given request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return \Drupal\Core\PageCache\StorageInterface|NULL
+   *   An appropriate storage or NULL if none is available.
+   */
+  public function selectStorage(Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/DelegatePageCacheFactory.php b/core/lib/Drupal/Core/PageCache/DelegatePageCacheFactory.php
new file mode 100644
index 0000000..8222194
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/DelegatePageCacheFactory.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\DelegatePageCacheFactory
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel exists.
+ */
+class DelegatePageCacheFactory extends PageCacheFactoryBase {
+  /**
+   * The global policy to be used in order to test whether caching is allowed.
+   *
+   * @var Drupal\Core\PageCache\PolicyRuleInterface
+   */
+  protected $policy;
+
+  /**
+   * A list of storage selectors.
+   *
+   * @var \Drupal\Core\PageCache\DelegatePageCache\StorageSelectorInterface[]
+   */
+  protected $storageSelectors;
+
+  /**
+   * A list of cache control selectors.
+   *
+   * @var \Drupal\Core\PageCache\DelegatePageCache\CacheControlSelectorInterface[]
+   */
+  protected $cacheControlSelectors;
+
+  /**
+   * Construct new default page cache factory.
+   *
+   * @param \Drupal\Core\PageCache\PolicyRuleInterface $policy
+   *   The global policy to be used in order to test whether caching is allowed.
+   */
+  public function __construct(PolicyRuleInterface $policy) {
+    $this->policy = $policy;
+    $this->storageSelectors = array();
+    $this->cacheControlSelectors = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getModestPageCache() {
+    return new DelegatePageCache($this->policy, $this->storageSelectors, $this->cacheControlSelectors);
+  }
+
+  /**
+   * Add one storage selector to the list of selectors.
+   *
+   * @param \Drupal\Core\PageCache\DelegatePageCache\StorageSelectorInterface $storage_selector
+   *   A storage selector.
+   */
+  public function addStorageSelector(DelegatePageCache\StorageSelectorInterface $storage_selector) {
+    $this->storageSelectors[] = $storage_selector;
+  }
+
+  /**
+   * Add one cache control selector to the list of selectors.
+   *
+   * @param \Drupal\Core\PageCache\DelegatePageCache\CacheControlSelectorInterface $cache_control_selector
+   *   A cache control selector.
+   */
+  public function addCacheControlSelector(DelegatePageCache\CacheControlSelectorInterface $cache_control_selector) {
+    $this->cacheControlSelectors[] = $cache_control_selector;
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/FastPageCacheDatabaseFactory.php b/core/lib/Drupal/Core/PageCache/FastPageCacheDatabaseFactory.php
new file mode 100644
index 0000000..d0ddd68
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/FastPageCacheDatabaseFactory.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\InternalCachePreKernelFactory.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Drupal\Core\Cache\DatabaseBackend;
+use Drupal\Core\ContentNegotiation;
+use Drupal\Core\Database\Database;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel exists.
+ */
+class FastPageCacheDatabaseFactory extends FastPageCacheFactoryBase {
+  /**
+   * {@inheritdoc}
+   */
+  public function getStorage(Settings $settings, Request $request) {
+    $bin = new DatabaseBackend(Database::getConnection(), 'page');
+
+    $content_negotiation = new ContentNegotiation();
+    $cid_generator = new Storage\CidGenerator($content_negotiation);
+    $use_compression = $settings->get('page_cache_response_gzip', FALSE);
+
+    return new Storage\InternalCache($bin, $cid_generator, $use_compression);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryBase.php b/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryBase.php
new file mode 100644
index 0000000..b115c21
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryBase.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\FastPageCacheFactoryBase
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel is available.
+ */
+abstract class FastPageCacheFactoryBase implements FastPageCacheFactoryInterface {
+  /**
+   * Return a pre kernel page cache instance for this request.
+   */
+  public function getFastPageCache(Settings $settings, Request $request) {
+    $policy = $this->getPolicy($settings, $request);
+    $storage = $this->getStorage($settings, $request);
+    $cache_control = $this->getCacheControl($settings, $request);
+
+    if ($policy && $storage && $cache_control) {
+      $page_cache = new PageCacheBase($policy);
+      $page_cache->setStorage($storage);
+      $page_cache->setCacheControl($cache_control);
+
+      return $page_cache;
+    }
+  }
+
+  /**
+   * Return the global page cache policy.
+   *
+   * @return \Drupal\Core\PageCache\PolicyRuleInterface
+   *   The global policy to be used in order to test whether caching is allowed.
+   */
+  protected function getPolicy(Settings $settings, Request $request) {
+    $policy = new CompoundPolicy();
+    $policy->add(new PolicyRule\DenyCommandLineOrUnsafeMethod());
+    $policy->add(new PolicyRule\DenyOpenSession());
+
+    return $policy;
+  }
+
+  /**
+   * Return an appropriate storage backend.
+   *
+   * @return \Drupal\Core\PageCache\StorageInterface|NULL
+   *   The storage to be used for retrieving / recording cached pages.
+   */
+  protected abstract function getStorage(Settings $settings, Request $request);
+
+  /**
+   * Construct a cache control instance.
+   *
+   * @return \Drupal\Core\PageCache\CacheControlInterface|NULL
+   *   The cache control to be used when delivering cacheable pages.
+   */
+  protected function getCacheControl(Settings $settings, Request $request) {
+    $max_age = $settings->get('page_cache_max_age');
+    $omit_vary_cookie = $settings->get('omit_vary_cookie');
+
+    if ($max_age > 0) {
+      return new CacheControl\PublicExternal($max_age, $omit_vary_cookie);
+    }
+    else {
+      return new CacheControl\NoCache();
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryInterface.php b/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryInterface.php
new file mode 100644
index 0000000..a423fa2
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/FastPageCacheFactoryInterface.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\FastPageCacheFactoryInterface
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel is available.
+ */
+interface FastPageCacheFactoryInterface {
+  /**
+   * Return a pre kernel page cache instance for this request.
+   */
+  public function getFastPageCache(Settings $settings, Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheBase.php b/core/lib/Drupal/Core/PageCache/PageCacheBase.php
new file mode 100644
index 0000000..fa04cbd
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheBase.php
@@ -0,0 +1,183 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCache.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Base class for page cache implementations.
+ */
+class PageCacheBase implements PageCacheInterface {
+  /**
+   * TRUE when current request can be saved to the cache, FALSE otherwise.
+   *
+   * @var bool
+   */
+  protected $cacheable = FALSE;
+
+  /**
+   * A policy rule used to decide whether a page request is cacheable.
+   *
+   * @var \Drupal\Core\PageCache\PolicyRuleInterface
+   */
+  protected $policy;
+
+  /**
+   * Storage backend for internal page cache.
+   *
+   * @var \Drupal\Core\PageCache\StorageInterface
+   */
+  protected $storage;
+
+  /**
+   * Cache control implementation governing external caches.
+   *
+   * @var \Drupal\Core\PageCache\CacheControlInterface
+   */
+  protected $cacheControl;
+
+  /**
+   * Construct new basic page cache instance.
+   *
+   * @param \Drupal\Core\PageCache\PolicyRuleInterface $policy
+   *   A policy rule used to decide whether a page request is cacheable.
+   */
+  public function __construct(PolicyRuleInterface $policy) {
+    $this->policy = $policy;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function handle(Request $request) {
+    $this->prepareCacheGet($request);
+
+    if (!$this->isCacheable()) {
+      return;
+    }
+
+    $storage = $this->getStorage();
+    if (!$storage) {
+      return;
+    }
+
+    $response = $storage->cacheGet($request);
+    if (!$response) {
+      return;
+    }
+
+    $this->prepareCacheHit($response, $request);
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function record(Response $response, Request $request) {
+    $this->prepareCacheSet($response, $request);
+
+    if ($this->isCacheable($response, $request)) {
+      $storage = $this->getStorage();
+      if ($storage) {
+        $storage->cacheSet($response, $request);
+      }
+
+      $this->prepareCacheMiss($response, $request);
+    }
+    else {
+      $this->prepareCachePass($response, $request);
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setNotCacheable() {
+    $this->cacheable = FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isCacheable() {
+    return $this->cacheable;
+  }
+
+  /**
+   * Set a storage backend.
+   */
+  public function setStorage(StorageInterface $storage) {
+    $this->storage = $storage;
+  }
+
+  /**
+   * Return the current storage backend.
+   */
+  public function getStorage() {
+    return $this->storage;
+  }
+
+  /**
+   * Set a cache-control backend.
+   */
+  public function setCacheControl(CacheControlInterface $cache_control) {
+    $this->cacheControl = $cache_control;
+  }
+
+  /**
+   * Return the current cache-control backend.
+   */
+  public function getCacheControl() {
+    return $this->cacheControl;
+  }
+
+  /**
+   * Apply the cache policy before attempting to retrieve a page from the cache.
+   */
+  protected function prepareCacheGet(Request $request) {
+    $this->cacheable = TRUE;
+
+    if (!$this->policy->apply($request)) {
+      $this->setNotCacheable();
+    }
+  }
+
+  /**
+   * Apply the policy rules before saving a page to the cache.
+   */
+  protected function prepareCacheSet(Response $response, Request $request) {
+  }
+
+  /**
+   * Prepare response headers before delivering a cacheable page to the client.
+   */
+  protected function prepareCacheHit(Response $response, Request $request) {
+    $cache_control = $this->getCacheControl() ?: new CacheControl\NoCache();
+    $cache_control->setCacheable($response, $request);
+  }
+
+  /**
+   * Prepare response headers before delivering a cacheable page to the client.
+   */
+  protected function prepareCacheMiss(Response $response, Request $request) {
+    $cache_control = $this->getCacheControl() ?: new CacheControl\NoCache();
+    $cache_control->setCacheable($response, $request);
+  }
+
+  /**
+   * Prepare headers on a non-cacheable response.
+   */
+  protected function prepareCachePass(Response $response, Request $request) {
+    $cache_control = $this->getCacheControl() ?: new CacheControl\NoCache();
+    $cache_control->setNotCacheable($response, $request);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheFactoryBase.php b/core/lib/Drupal/Core/PageCache/PageCacheFactoryBase.php
new file mode 100644
index 0000000..4752bff
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheFactoryBase.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheFactoryBase
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel exists.
+ */
+abstract class PageCacheFactoryBase implements PageCacheFactoryInterface {
+  /**
+   * {@inheritdoc}
+   */
+  public function getPageCache() {
+    $helper = $this->getPageCacheHelper();
+    if ($helper->existsFastPageCacheInstance()) {
+      return $helper->getFastPageCacheInstance();
+    }
+    else {
+      return $this->getModestPageCache();
+    }
+  }
+
+  /**
+   * Return a page cache helper instance.
+   *
+   * @return \Drupal\Core\PageCache\PageCacheHelper
+   *   A page cache heler class to use.
+   */
+  protected function getPageCacheHelper() {
+    return new PageCacheHelper();
+  }
+
+  /**
+   * Construct and return a new page cache instance.
+   *
+   * @return \Drupal\Core\PageCache\PageCacheInterface
+   *   A page cache implementation.
+   */
+  protected abstract function getModestPageCache();
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php b/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php
new file mode 100644
index 0000000..747cc12
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheFactoryInterface.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheFactoryInterface
+ */
+
+namespace Drupal\Core\PageCache;
+
+/**
+ * A page cache factory operating before the kernel is available.
+ */
+interface PageCacheFactoryInterface {
+  /**
+   * Return page cache instance.
+   *
+   * Must return the fast page cache instance when it exists. Otherwise
+   * construct a new one one and return it.
+   *
+   * @return \Drupal\Core\PageCache\PageCacheInterface
+   *   A page cache implementation.
+   */
+  public function getPageCache();
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheHelper.php b/core/lib/Drupal/Core/PageCache/PageCacheHelper.php
new file mode 100644
index 0000000..502839a
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheHelper.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheHelper.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Drupal\Component\Utility\Settings;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Provides two alternative ways of cache retrieval.
+ *
+ * Delivery of pages from the cache can happen either before or after the kernel
+ * and service container is initialized. This class is used in tandem with
+ * PageCacheFactoryBase. The latter will ensure that a page cache service
+ * instantiated before the kernel existed, still will be present in the service
+ * container.
+ */
+class PageCacheHelper {
+  /**
+   * Singleton instance of pre-kernel page cache service.
+   *
+   * @var \Drupal\Core\PageCache\PageCacheInterface
+   */
+  private static $fastPageCache;
+
+  /**
+   * Return singleton page cache instance.
+   *
+   * @return \Drupal\Core\PageCache\PageCacheInterface|NULL
+   *   Page cache instance or NULL.
+   */
+  public function getFastPageCacheInstance() {
+    return self::$fastPageCache;
+  }
+
+  /**
+   * Set new singleton page cache instance.
+   */
+  public function setFastPageCacheInstance(PageCacheInterface $page_cache) {
+    self::$fastPageCache = $page_cache;
+  }
+
+  /**
+   * Return TRUE if the singleton cache instance already exists.
+   */
+  public function existsFastPageCacheInstance() {
+    return (bool) self::$fastPageCache;
+  }
+
+  /**
+   * Try to deliver a response from the cache before the kernel exists.
+   */
+  public function deliverFromCacheBeforeKernel(Settings $settings, Request $request) {
+    $factory_class = $settings->get('page_cache_factory');
+    if (!class_exists($factory_class)) {
+      return;
+    }
+
+    $factory = new $factory_class();
+
+    $page_cache = $factory->getFastPageCache($settings, $request);
+    if (!$page_cache) {
+      return;
+    }
+
+    $this->setFastPageCacheInstance($page_cache);
+
+    $response = $page_cache->handle($request);
+    if ($response) {
+      $this->deliver($response, $request);
+      return TRUE;
+    }
+  }
+
+  /**
+   * Try to deliver a response from the cache.
+   */
+  public function deliverFromCacheAfterContainer(PageCacheInterface $page_cache, Request $request) {
+    if (!$this->existsFastPageCacheInstance()) {
+      $response = $page_cache->handle($request);
+      if ($response) {
+        $this->deliver($response, $request);
+        return TRUE;
+      }
+    }
+  }
+
+  /**
+   * After a page build, record and deliver the response.
+   *
+   * Save the rendered markup to the page cache and add appropriate headers
+   * instructing external caches like browsers and proxies if necessary.
+   */
+  public function recordAndDeliver(PageCacheInterface $page_cache, Response $response, Request $request) {
+    if ($this->existsFastPageCacheInstance()) {
+      $page_cache = $this->getFastPageCacheInstance();
+    }
+    $page_cache->record($response, $request);
+    $this->deliver($response, $request);
+  }
+
+  /**
+   * Prepare and send a response.
+   */
+  protected function deliver(Response $response, Request $request) {
+    $response->prepare($request)->send();
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PageCacheInterface.php b/core/lib/Drupal/Core/PageCache/PageCacheInterface.php
new file mode 100644
index 0000000..1abee59
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PageCacheInterface.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PageCacheInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Look up requests in the page cache and record responses.
+ */
+interface PageCacheInterface {
+  /**
+   * Try to retrieve a page from the cache and return the response object.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The incoming request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|null
+   *   A response object if the lookup in any cache backend with internal
+   *   storage succeeded. Otherwise null.
+   */
+  public function handle(Request $request);
+
+  /**
+   * If caching is allowed, save the response in the page cache.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object complete with headers and content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object.
+   */
+  public function record(Response $response, Request $request);
+
+  /**
+   * Disable any page caching for the current request.
+   */
+  public function setNotCacheable();
+
+  /**
+   * Test whether the current page request is cacheable.
+   *
+   * @return bool
+   *   Return TRUE when the response to the current request is cacheable.
+   */
+  public function isCacheable();
+}
diff --git a/core/lib/Drupal/Core/PageCache/PolicyRule/DenyCommandLineOrUnsafeMethod.php b/core/lib/Drupal/Core/PageCache/PolicyRule/DenyCommandLineOrUnsafeMethod.php
new file mode 100644
index 0000000..351b36c
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PolicyRule/DenyCommandLineOrUnsafeMethod.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PolicyRule\DenyCommandLineOrUnsafeMethod.
+ */
+
+namespace Drupal\Core\PageCache\PolicyRule;
+
+use Drupal\Core\PageCache\PolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Reject when running from the command line or when HTTP method is not safe.
+ *
+ * An instance of this class is normally used as the global policy by the main
+ * page caching service. The policy allows caching as long as the request was
+ * not initiated from the command line interface (drush) and the request method
+ * is either GET or HEAD (see RFC 2616, section 9.1.1 - Safe Methods).
+ *
+ * Note that this policy does not exclude open sessions from caching. Therefore
+ * it needs to be combined with additional policy rules in order to implement a
+ * safe page caching service. E.g. only allow storage to / retrieval from the
+ * internal page cache when DenyOpenSessionPolicy evaluates to TRUE.
+ */
+class DenyCommandLineOrUnsafeMethod implements PolicyRuleInterface {
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return $this->isNotCLIRequest($request) && $request->isMethodSafe();
+  }
+
+  /**
+   * Exclude a page request when run from a command line script.
+   */
+  protected function isNotCLIRequest(Request $request) {
+    return !drupal_is_cli();
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PolicyRule/DenyOpenSession.php b/core/lib/Drupal/Core/PageCache/PolicyRule/DenyOpenSession.php
new file mode 100644
index 0000000..c9e83ad
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PolicyRule/DenyOpenSession.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PolicyRule\DenyOpenSession.
+ */
+
+namespace Drupal\Core\PageCache\PolicyRule;
+
+use Drupal\Core\PageCache\PolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule evaluating to TRUE only when there is no session open.
+ *
+ * Do not serve cached pages to authenticated users, or to anonymous users when
+ * $_SESSION is non-empty. $_SESSION may contain status messages from a form
+ * submission, the contents of a shopping cart, or other userspecific content
+ * that should not be cached and displayed to other users.
+ */
+class DenyOpenSession implements PolicyRuleInterface {
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return !$request->cookies->has(session_name());
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PolicyRule/InternalCacheConfig.php b/core/lib/Drupal/Core/PageCache/PolicyRule/InternalCacheConfig.php
new file mode 100644
index 0000000..3094e86
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PolicyRule/InternalCacheConfig.php
@@ -0,0 +1,42 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PolicyRule\InternalCacheConfig.
+ */
+
+namespace Drupal\Core\PageCache\PolicyRule;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\PageCache\PolicyRuleInterface;
+use Drupal\Core\PageCache\PolicyRule\InternalCacheConfig;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule evaluating to TRUE when internal cache is enabled.
+ */
+class InternalCacheConfig implements PolicyRuleInterface {
+  /**
+   * Config object for system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
+   * Construct new internal cache config policy.
+   *
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The configuration factory object.
+   */
+  public function __construct(ConfigFactory $config_factory) {
+    $this->config = $config_factory->get('system.performance');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return $this->config->get('cache.page.use_internal');
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PolicyRule/MaxAgeConfig.php b/core/lib/Drupal/Core/PageCache/PolicyRule/MaxAgeConfig.php
new file mode 100644
index 0000000..8e7ceff
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PolicyRule/MaxAgeConfig.php
@@ -0,0 +1,41 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PolicyRule\MaxAgeConfig.
+ */
+
+namespace Drupal\Core\PageCache\PolicyRule;
+
+use Drupal\Core\Config\ConfigFactory;
+use Drupal\Core\PageCache\PolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A policy rule evaluating to TRUE when a maximum age is configured.
+ */
+class MaxAgeConfig implements PolicyRuleInterface {
+  /**
+   * Config object for system performance configuration.
+   *
+   * @var \Drupal\Core\Config\Config
+   */
+  protected $config;
+
+  /**
+   * Construct new page cache max_age policy.
+   *
+   * @param \Drupal\Core\Config\ConfigFactory $config_factory
+   *   The configuration factory object.
+   */
+  public function __construct(ConfigFactory $config_factory) {
+    $this->config = $config_factory->get('system.performance');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function apply(Request $request) {
+    return $this->config->get('cache.page.max_age') > 0;
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/PolicyRuleInterface.php b/core/lib/Drupal/Core/PageCache/PolicyRuleInterface.php
new file mode 100644
index 0000000..1db0213
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/PolicyRuleInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\PolicyRuleInterface.
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines the interface for cache policy rule implementations.
+ */
+interface PolicyRuleInterface {
+  /**
+   * Apply the policy rule to the given request and return the result.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   A request object.
+   *
+   * @return bool
+   *   TRUE when the policy rule allows caching for the request.
+   */
+  public function apply(Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php b/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php
new file mode 100644
index 0000000..ee7b1f6
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CidGenerator.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CidGenerator.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Core\ContentNegotiation;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Default page cache-id generator.
+ */
+class CidGenerator implements CidGeneratorInterface {
+  /**
+   * Content negotiation service.
+   *
+   * @var \Drupal\Core\ContentNegotiation
+   */
+  protected $negotiation;
+
+  /**
+   * Constructs a new page cache-id generator.
+   *
+   * @param \Drupal\Core\ContentNegotiation $negotiation
+   *   Content negotiation service.
+   */
+  public function __construct(ContentNegotiation $negotiation) {
+    $this->negotiation = $negotiation;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCid(Request $request) {
+    $cid_parts = array(
+      $request->getUri(),
+      $this->negotiation->getContentType($request),
+    );
+    return sha1(implode(':', $cid_parts));
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php b/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php
new file mode 100644
index 0000000..aae509d
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CidGeneratorInterface.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CidGeneratorInterface.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Defines an interface for page cache-id generators.
+ */
+interface CidGeneratorInterface {
+  /**
+   * Generate the page cache cid for a request.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request for this page.
+   *
+   * @return string
+   *   The cid for a request.
+   */
+  public function getCid(Request $request);
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php b/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php
new file mode 100644
index 0000000..f50cfb3
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/CompressionHelper.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\CompressionHelper.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Decompress a response for clients not accepting gzip encoded content.
+ */
+class CompressionHelper {
+  /**
+   * Compress the response body and add appropriate headers.
+   */
+  public function compress(Response $response, Request $request) {
+    if (extension_loaded('zlib') && !$response->headers->get('Content-Encoding')) {
+      $content = $response->getContent();
+      if ($content) {
+        $response->setContent(gzencode($content, 9, FORCE_GZIP));
+        $response->headers->set('Content-Encoding', 'gzip');
+      }
+
+      // When page compression is enabled, ensure that proxy caches will record
+      // and deliver different versions of a page depending on whether the
+      // client supports gzip or not.
+      $response->setVary('Accept-Encoding', FALSE);
+    }
+  }
+
+  /**
+   * Uncompress a response body if necessary.
+   */
+  public function uncompress(Response $response, Request $request) {
+    if (extension_loaded('zlib') && $response->headers->get('Content-Encoding') == 'gzip') {
+      if (strpos($request->headers->get('Accept-Encoding'), 'gzip') === FALSE) {
+        // The client does not support compression. Decompress the content and
+        // remove the Content-Encoding header.
+        $content = $response->getContent();
+        $content = gzinflate(substr(substr($content, 10), 0, -8));
+        $response->setContent($content);
+        $response->headers->remove('Content-Encoding');
+      }
+      else {
+        // The response content is already gzip'ed, so make sure
+        // zlib.output_compression does not compress it once more.
+        ini_set('zlib.output_compression', '0');
+      }
+    }
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php b/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php
new file mode 100644
index 0000000..83d8809
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/InternalCache.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\InternalCache.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\PageCache\StorageInterface;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Cache storage backend based on the internal cache.
+ */
+class InternalCache implements StorageInterface {
+  /**
+   * Cache backend for the page cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * Generator for the cache id.
+   *
+   * @var \Drupal\Core\PageCache\CidGeneratorInterface
+   */
+  protected $cidGenerator;
+
+  /**
+   * A flag indicating whether compression of cached objects should be enabled.
+   *
+   * @var bool
+   */
+  protected $useCompression;
+
+  /**
+   * Construct a new cache backend with internal cache storage.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   The cache backend to use.
+   * @param \Drupal\Core\PageCache\Storage\CidGeneratorInterface $cid_generator
+   *   Generator for the cache id.
+   * @param bool $use_compression
+   *   A flag indicating whether compression of cached objects should be
+   *   enabled.
+   */
+  public function __construct(CacheBackendInterface $cache, CidGeneratorInterface $cid_generator, $use_compression) {
+    $this->cache = $cache;
+    $this->cidGenerator = $cid_generator;
+    $this->useCompression = $use_compression;
+  }
+
+  /**
+   * Retrieve a response object from the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The incoming request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|NULL
+   *   A response object complete with headers and content.
+   */
+  public function cacheGet(Request $request) {
+    $cid = $this->cidGenerator->getCid($request);
+    $cache = $this->cache->get($cid);
+    if (!is_object($cache)) {
+      return;
+    }
+
+    // Prepare a new response and set the content.
+    $response = new Response($cache->data['body'], $cache->data['status'], $cache->data['headers']);
+
+    // If there is no Last-Modified header on the cached response, compute it
+    // from the creation date of the cache-object.
+    if (!$response->headers->has('Last-Modified')) {
+      $date = new \DateTime();
+      $date->setTimestamp($cache->created);
+      $response->setLastModified($date);
+    }
+
+    // Uncompress the response when serving clients which cannot handle gzip.
+    // In order to prevent garbled pages when server configuration changes,
+    // decompressing the response is done unconditionally.
+    $this->getCompressionHelper()->uncompress($response, $request);
+
+    $response->headers->set('X-Drupal-Cache', 'HIT');
+
+    return $response;
+  }
+
+  /**
+   * Save a response object to the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object complete with headers and content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object the response was generated for.
+   */
+  public function cacheSet(Response $response, Request $request) {
+    // Use the actual timestamp from an Expires header, if available.
+    if ($date = $response->getExpires()) {
+      $expire = $date->getTimestamp();
+    }
+    else {
+      $expire = Cache::PERMANENT;
+    }
+
+    if ($this->useCompression) {
+      $compression_helper = $this->getCompressionHelper();
+      $compression_helper->compress($response, $request);
+    }
+
+    $cid = $this->cidGenerator->getCid($request);
+    $data = array(
+      'status' => $response->getStatusCode(),
+      'headers' => $response->headers->all(),
+      'body' => $response->getContent(),
+    );
+    $tags = array('content' => TRUE) + drupal_cache_tags_page_get($response);
+    $this->cache->set($cid, $data, $expire, $tags);
+
+    // Uncompress the response when serving clients which cannot handle gzip.
+    if (isset($compression_helper)) {
+      $compression_helper->uncompress($response, $request);
+    }
+
+    $response->headers->set('X-Drupal-Cache', 'MISS');
+  }
+
+  /**
+   * Construct and return the compression helper.
+   *
+   * @return \Drupal\Core\PageCache\Storage\CompressionHelper
+   *   A helper object implementing compression/uncompression of responses.
+   */
+  public function getCompressionHelper() {
+    return new CompressionHelper();
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/Storage/InternalCacheFactory.php b/core/lib/Drupal/Core/PageCache/Storage/InternalCacheFactory.php
new file mode 100644
index 0000000..4a4b9f8
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/Storage/InternalCacheFactory.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\Storage\InternalCacheFactory.
+ */
+
+namespace Drupal\Core\PageCache\Storage;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * A page cache factory operating before the kernel exists.
+ */
+class InternalCacheFactory {
+  /**
+   * A cache bin to use as the page cache.
+   *
+   * @var \Drupal\Core\Cache\CacheBackendInterface
+   */
+  protected $cache;
+
+  /**
+   * A cache id generator.
+   *
+   * @var \Drupal\Core\PageCache\Storage\CidGeneratorInterface
+   */
+  protected $cidGenerator;
+
+  /**
+   * A flag indicating whether compression of cached objects should be enabled.
+   *
+   * @var bool
+   */
+  protected $useCompression;
+
+  /**
+   * Construct page cache storage factory for internal cache storage backend.
+   *
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
+   *   A cache bin to use as the page cache.
+   * @param \Drupal\Core\PageCache\Storage\CidGeneratorInterface $cid_generator
+   *   A cache id generator.
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The configuration factory instance.
+   */
+  public function __construct(CacheBackendInterface $cache, CidGeneratorInterface $cid_generator, ConfigFactoryInterface $config_factory) {
+    $this->cache = $cache;
+    $this->cidGenerator = $cid_generator;
+    $this->useCompression = $config_factory->get('system.performance')->get('response.gzip');
+  }
+
+  /**
+   * Construct pre-kernel storage implementation.
+   *
+   * @return \Drupal\Core\PageCache\PageCacheInterface
+   *   A page cache implementation.
+   */
+  public function getInternalCache() {
+    return new InternalCache($this->cache, $this->cidGenerator, $this->useCompression);
+  }
+}
diff --git a/core/lib/Drupal/Core/PageCache/StorageInterface.php b/core/lib/Drupal/Core/PageCache/StorageInterface.php
new file mode 100644
index 0000000..ce9ad91
--- /dev/null
+++ b/core/lib/Drupal/Core/PageCache/StorageInterface.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\StorageInterface
+ */
+
+namespace Drupal\Core\PageCache;
+
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+
+/**
+ * Defines an interface for internal page cache storage.
+ */
+interface StorageInterface {
+  /**
+   * Retrieve a response object from the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The incoming request object.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response|NULL
+   *   A response object complete with headers and content.
+   */
+  public function cacheGet(Request $request);
+
+  /**
+   * Save a response object to the page cache storage.
+   *
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   *   A response object complete with headers and content.
+   * @param \Symfony\Component\HttpFoundation\Request $request
+   *   The request object the response was generated for.
+   */
+  public function cacheSet(Response $response, Request $request);
+}
diff --git a/core/modules/statistics/statistics.php b/core/modules/statistics/statistics.php
index 03908f2..887cfa0 100644
--- a/core/modules/statistics/statistics.php
+++ b/core/modules/statistics/statistics.php
@@ -11,6 +11,7 @@
 // Load the Drupal bootstrap.
 require_once dirname(dirname(__DIR__)) . '/vendor/autoload.php';
 require_once dirname(dirname(__DIR__)) . '/includes/bootstrap.inc';
+require_once dirname(dirname(__DIR__)) . '/includes/database.inc';
 drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 
 if (\Drupal::config('statistics.settings')->get('count_content_views')) {
diff --git a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
index 3ca1f19..8cd1923 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Bootstrap/PageCacheTest.php
@@ -157,7 +157,7 @@ function testPageCache() {
     // Fill the cache.
     $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'MISS', 'Page was not cached.');
-    $this->assertEqual(strtolower($this->drupalGetHeader('Vary')), 'cookie,accept-encoding', 'Vary header was sent.');
+    $this->assertEqual(strtolower($this->drupalGetHeader('Vary')), 'accept-encoding,cookie', 'Vary header was sent.');
     // Symfony's Response logic determines a specific order for the subvalues
     // of the Cache-Control header, even if they are explicitly passed in to
     // the response header bag in a different order.
@@ -168,7 +168,7 @@ function testPageCache() {
     // Check cache.
     $this->drupalGet('system-test/set-header', array('query' => array('name' => 'Foo', 'value' => 'bar')));
     $this->assertEqual($this->drupalGetHeader('X-Drupal-Cache'), 'HIT', 'Page was cached.');
-    $this->assertEqual(strtolower($this->drupalGetHeader('Vary')), 'cookie,accept-encoding', 'Vary: Cookie header was sent.');
+    $this->assertEqual(strtolower($this->drupalGetHeader('Vary')), 'accept-encoding,cookie', 'Vary: Cookie header was sent.');
     $this->assertEqual($this->drupalGetHeader('Cache-Control'), 'max-age=300, public', 'Cache-Control header was sent.');
     $this->assertEqual($this->drupalGetHeader('Expires'), 'Sun, 19 Nov 1978 05:00:00 GMT', 'Expires header was sent.');
     $this->assertEqual($this->drupalGetHeader('Foo'), 'bar', 'Custom header was sent.');
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
index d5fc878..d099ed6 100644
--- a/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
+++ b/core/modules/toolbar/lib/Drupal/toolbar/Controller/ToolbarController.php
@@ -23,7 +23,6 @@ class ToolbarController extends ControllerBase {
    * @return \Symfony\Component\HttpFoundation\JsonResponse
    */
   public function subtreesJsonp() {
-    _toolbar_initialize_page_cache();
     $subtrees = toolbar_get_rendered_subtrees();
     $response = new JsonResponse($subtrees);
     $response->setCallback('Drupal.toolbar.setSubtrees.resolve');
diff --git a/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPathPolicyRule.php b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPathPolicyRule.php
new file mode 100644
index 0000000..5f10737
--- /dev/null
+++ b/core/modules/toolbar/lib/Drupal/toolbar/PageCache/ToolbarPathPolicyRule.php
@@ -0,0 +1,26 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\PageCache\ToolbarPathPolicyRule.
+ */
+
+namespace Drupal\toolbar\PageCache;
+
+use Drupal\Core\PageCache\PolicyRuleInterface;
+use Symfony\Component\HttpFoundation\Request;
+
+/**
+ * Cache policy for the toolbar page cache service.
+ *
+ * This policy allows caching of requests directed to /toolbar/subtrees/{hash}
+ * even for authenticated users.
+ */
+class ToolbarPathPolicyRule implements PolicyRuleInterface {
+  /**
+   * Test whether this request goes to /toolbar/subtrees.
+   */
+  public function apply(Request $request) {
+    return strpos($request->getPathInfo(), '/toolbar/subtrees/') === 0;
+  }
+}
diff --git a/core/modules/toolbar/toolbar.module b/core/modules/toolbar/toolbar.module
index 7217986..6c1db43 100644
--- a/core/modules/toolbar/toolbar.module
+++ b/core/modules/toolbar/toolbar.module
@@ -104,48 +104,6 @@ function toolbar_element_info() {
 }
 
 /**
- * Use Drupal's page cache for toolbar/subtrees/*, even for authenticated users.
- *
- * This gets invoked after full bootstrap, so must duplicate some of what's
- * done by _drupal_bootstrap_page_cache().
- *
- * @todo Replace this hack with something better integrated with DrupalKernel
- *   once Drupal's page caching itself is properly integrated.
- */
-function _toolbar_initialize_page_cache() {
-  $GLOBALS['conf']['system.performance']['cache']['page']['enabled'] = TRUE;
-  drupal_page_is_cacheable(TRUE);
-
-  // If we have a cache, serve it.
-  // @see _drupal_bootstrap_page_cache()
-  $request = \Drupal::request();
-  $cache = drupal_page_get_cache($request);
-  if (is_object($cache)) {
-    $response = new Response();
-    $response->headers->set('X-Drupal-Cache', 'HIT');
-    date_default_timezone_set(drupal_get_user_timezone());
-
-    drupal_serve_page_from_cache($cache, $response, $request);
-
-    $response->prepare($request);
-    $response->send();
-    // We are done.
-    exit;
-  }
-
-  // Otherwise, create a new page response (that will be cached).
-  drupal_add_http_header('X-Drupal-Cache', 'MISS');
-
-  // The Expires HTTP header is the heart of the client-side HTTP caching. The
-  // additional server-side page cache only takes effect when the client
-  // accesses the callback URL again (e.g., after clearing the browser cache or
-  // when force-reloading a Drupal page).
-  $max_age = 3600 * 24 * 365;
-  drupal_add_http_header('Expires', gmdate(DATE_RFC1123, REQUEST_TIME + $max_age));
-  drupal_add_http_header('Cache-Control', 'private, max-age=' . $max_age);
-}
-
-/**
  * Implements hook_page_build().
  *
  * Add admin toolbar to the page_top region automatically.
diff --git a/core/modules/toolbar/toolbar.services.yml b/core/modules/toolbar/toolbar.services.yml
index 7f26968..8e86b7a 100644
--- a/core/modules/toolbar/toolbar.services.yml
+++ b/core/modules/toolbar/toolbar.services.yml
@@ -6,3 +6,23 @@ services:
     factory_method: get
     factory_service: cache_factory
     arguments: [toolbar]
+  toolbar.page_cache_policy.toolbar_path:
+    class: Drupal\toolbar\PageCache\ToolbarPathPolicyRule
+  toolbar.page_cache_selector.internal_storage:
+    class: Drupal\Core\PageCache\DelegatePageCache\PolicyBoundStorageSelector
+    arguments: ['@page_cache.internal_storage']
+    calls:
+      - [add, ['@page_cache_policy', '@page_cache_policy.use_internal_config']]
+      - [add, ['@page_cache_policy', '@toolbar.page_cache_policy.toolbar_path']]
+    tags:
+      - { name: delegate_page_cache.storage_selector }
+  toolbar.page_cache_selector.private_browser_cache:
+    class: Drupal\Core\PageCache\DelegatePageCache\PolicyBoundCacheControlSelector
+    arguments: ['@toolbar.page_cache.private_browser_cache']
+    calls:
+      - [add, ['@page_cache_policy', '@toolbar.page_cache_policy.toolbar_path']]
+    tags:
+      - { name: delegate_page_cache.cache_control_selector }
+  toolbar.page_cache.private_browser_cache:
+    class: Drupal\Core\PageCache\CacheControl\PrivateBrowser
+    arguments: [31536000]
diff --git a/core/update.php b/core/update.php
index 433d5cf..67d98d4 100644
--- a/core/update.php
+++ b/core/update.php
@@ -333,7 +333,7 @@ function update_task_list($active = NULL) {
 \Drupal::getContainer()->set('request', $request);
 
 // Determine if the current user has access to run update.php.
-drupal_bootstrap(DRUPAL_BOOTSTRAP_PAGE_CACHE);
+drupal_bootstrap(DRUPAL_BOOTSTRAP_KERNEL);
 
 require_once DRUPAL_ROOT . '/' . settings()->get('session_inc', 'core/includes/session.inc');
 drupal_session_initialize();
