diff --git a/core/lib/Drupal/Core/Access/AccessManager.php b/core/lib/Drupal/Core/Access/AccessManager.php
index 0d446ee7f19a9c7c28c91f213879fd73ec9b7d4b..baf916193ca1b5e7de5de9c7298888953c52c06d 100644
--- a/core/lib/Drupal/Core/Access/AccessManager.php
+++ b/core/lib/Drupal/Core/Access/AccessManager.php
@@ -104,7 +104,7 @@ public function checkNamedRoute($route_name, array $parameters = array(), Accoun
     catch (ParamNotConvertedException $e) {
       // Uncacheable because conversion of the parameter may not have been
       // possible due to dynamic circumstances.
-      $result = AccessResult::forbidden()->setCacheable(FALSE);
+      $result = AccessResult::forbidden()->setCacheMaxAge(0);
       return $return_as_object ? $result : $result->isAllowed();
     }
   }
diff --git a/core/lib/Drupal/Core/Access/AccessResult.php b/core/lib/Drupal/Core/Access/AccessResult.php
index bb7682101ee425f3370c3812b50dd267207a4c7e..6188b3407d18a7cba7e8ac4f206fe282145b458b 100644
--- a/core/lib/Drupal/Core/Access/AccessResult.php
+++ b/core/lib/Drupal/Core/Access/AccessResult.php
@@ -7,7 +7,7 @@
 namespace Drupal\Core\Access;
 
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Config\ConfigBase;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -26,14 +26,7 @@
  * When using ::orIf() and ::andIf(), cacheability metadata will be merged
  * accordingly as well.
  */
-abstract class AccessResult implements AccessResultInterface, CacheableInterface {
-
-  /**
-   * Whether the access result is cacheable.
-   *
-   * @var bool
-   */
-  protected $isCacheable;
+abstract class AccessResult implements AccessResultInterface, CacheableDependencyInterface {
 
   /**
    * The cache context IDs (to vary a cache item ID based on active contexts).
@@ -63,9 +56,9 @@ abstract class AccessResult implements AccessResultInterface, CacheableInterface
    * Constructs a new AccessResult object.
    */
   public function __construct() {
-    $this->setCacheable(TRUE)
-      ->resetCacheContexts()
+    $this->resetCacheContexts()
       ->resetCacheTags()
+      // Max-age must be non-zero for an access result to be cacheable.
       // Typically, cache items are invalidated via associated cache tags, not
       // via a maximum age.
       ->setCacheMaxAge(Cache::PERMANENT);
@@ -215,13 +208,6 @@ public function isNeutral() {
     return FALSE;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getCacheKeys() {
-    return [];
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -237,16 +223,6 @@ public function getCacheTags() {
     return $this->tags;
   }
 
-  /**
-   * {@inheritdoc}
-   *
-   * It's not very useful to cache individual access results, but the interface
-   * forces us to implement this method, so just use the default cache bin.
-   */
-  public function getCacheBin() {
-    return 'default';
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -254,26 +230,6 @@ public function getCacheMaxAge() {
     return $this->maxAge;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function isCacheable() {
-    return $this->isCacheable;
-  }
-
-  /**
-   * Sets whether this access result is cacheable. It is cacheable by default.
-   *
-   * @param bool $is_cacheable
-   *   Whether this access result is cacheable.
-   *
-   * @return $this
-   */
-  public function setCacheable($is_cacheable) {
-    $this->isCacheable = $is_cacheable;
-    return $this;
-  }
-
   /**
    * Adds cache contexts associated with the access result.
    *
@@ -392,19 +348,19 @@ public function orIf(AccessResultInterface $other) {
     $merge_other = FALSE;
     if ($this->isForbidden() || $other->isForbidden()) {
       $result = static::forbidden();
-      if (!$this->isForbidden() || (!$this->isCacheable() && $other->isForbidden())) {
+      if (!$this->isForbidden() || ($this->getCacheMaxAge() === 0 && $other->isForbidden())) {
         $merge_other = TRUE;
       }
     }
     elseif ($this->isAllowed() || $other->isAllowed()) {
       $result = static::allowed();
-      if (!$this->isAllowed() || (!$this->isCacheable() && $other->isAllowed())) {
+      if (!$this->isAllowed() || ($this->getCacheMaxAge() === 0 && $other->isAllowed())) {
         $merge_other = TRUE;
       }
     }
     else {
       $result = static::neutral();
-      if (!$this->isNeutral() || (!$this->isCacheable() && $other->isNeutral())) {
+      if (!$this->isNeutral() || ($this->getCacheMaxAge() === 0 && $other->isNeutral())) {
         $merge_other = TRUE;
       }
     }
@@ -446,8 +402,8 @@ public function andIf(AccessResultInterface $other) {
       // result must also not be cacheable, except if the other access result
       // has isForbidden() === TRUE. isForbidden() access results are contagious
       // in that they propagate regardless of the other value.
-      if (!$this->isCacheable() && !$result->isForbidden()) {
-        $result->setCacheable(FALSE);
+      if ($this->getCacheMaxAge() === 0 && !$result->isForbidden()) {
+        $result->setCacheMaxAge(0);
       }
     }
     return $result;
@@ -462,17 +418,21 @@ public function andIf(AccessResultInterface $other) {
    * @return $this
    */
   public function inheritCacheability(AccessResultInterface $other) {
-    if ($other instanceof CacheableInterface) {
-      $this->setCacheable($other->isCacheable());
+    if ($other instanceof CacheableDependencyInterface) {
+      if ($this->getCacheMaxAge() !== 0 && $other->getCacheMaxAge() !== 0) {
+        $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
+      }
+      else {
+        $this->setCacheMaxAge($other->getCacheMaxAge());
+      }
       $this->addCacheContexts($other->getCacheContexts());
       $this->addCacheTags($other->getCacheTags());
-      $this->setCacheMaxAge(Cache::mergeMaxAges($this->getCacheMaxAge(), $other->getCacheMaxAge()));
     }
     // If any of the access results don't provide cacheability metadata, then
     // we cannot cache the combined access result, for we may not make
     // assumptions.
     else {
-      $this->setCacheable(FALSE);
+      $this->setCacheMaxAge(0);
     }
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
index 0331df963dca6b6ceae2553b1f0ebcd58e897e03..61b882def7cec2103cbe5a1458b1c27f8ad30a22 100644
--- a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
@@ -66,7 +66,7 @@ public function access(Route $route, Request $request, RouteMatchInterface $rout
       $result = AccessResult::forbidden();
     }
     // Not cacheable because the CSRF token is highly dynamic.
-    return $result->setCacheable(FALSE);
+    return $result->setCacheMaxAge(0);
   }
 
 }
diff --git a/core/lib/Drupal/Core/Block/BlockBase.php b/core/lib/Drupal/Core/Block/BlockBase.php
index 8bf86c50ec5c37a35e17b8e465895361a0c9972f..bf349cdfadebafbc1e6b9b7b1b63e4b2e0373717 100644
--- a/core/lib/Drupal/Core/Block/BlockBase.php
+++ b/core/lib/Drupal/Core/Block/BlockBase.php
@@ -131,7 +131,7 @@ public function access(AccountInterface $account, $return_as_object = FALSE) {
       $access = AccessResult::forbidden();
     }
 
-    $access->setCacheable(FALSE);
+    $access->setCacheMaxAge(0);
     return $return_as_object ? $access : $access->isAllowed();
   }
 
diff --git a/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..1d447a11b5ec75a38ef1264cd41d2865a6e303cc
--- /dev/null
+++ b/core/lib/Drupal/Core/Cache/CacheableDependencyInterface.php
@@ -0,0 +1,56 @@
+<?php
+/**
+ * @file
+ * Contains \Drupal\Core\CacheableDependencyInterface
+ */
+
+namespace Drupal\Core\Cache;
+
+/**
+ * Defines an interface for objects which may be used by other cached objects.
+ *
+ * All cacheability metadata exposed in this interface is bubbled to parent
+ * objects when they are cached: if a child object needs to be varied by certain
+ * cache contexts, invalidated by certain cache tags, expire after a certain
+ * maximum age, then so should any parent object.
+ *
+ * @ingroup cache
+ */
+interface CacheableDependencyInterface {
+
+  /**
+   * The cache contexts associated with this object.
+   *
+   * These identify a specific variation/representation of the object.
+   *
+   * Cache contexts are tokens: placeholders that are converted to cache keys by
+   * the @cache_contexts service. The replacement value depends on the request
+   * context (the current URL, language, and so on). They're converted before
+   * storing an object in cache.
+   *
+   * @return string[]
+   *   An array of cache context tokens, used to generate a cache ID.
+   *
+   * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
+   */
+  public function getCacheContexts();
+
+  /**
+   * The cache tags associated with this object.
+   *
+   * When this object is modified, these cache tags will be invalidated.
+   *
+   * @return string[]
+   *  A set of cache tags.
+   */
+  public function getCacheTags();
+
+  /**
+   * The maximum age for which this object may be cached.
+   *
+   * @return int
+   *   The maximum time in seconds that this object may be cached.
+   */
+  public function getCacheMaxAge();
+
+}
diff --git a/core/lib/Drupal/Core/Cache/CacheableInterface.php b/core/lib/Drupal/Core/Cache/CacheableInterface.php
index 70b0a8733b71b78fd6382cd9092205ab4ff7189a..f57f23644af67af85232caea0c02ad774430bdba 100644
--- a/core/lib/Drupal/Core/Cache/CacheableInterface.php
+++ b/core/lib/Drupal/Core/Cache/CacheableInterface.php
@@ -20,7 +20,7 @@
  *
  * @ingroup cache
  */
-interface CacheableInterface {
+interface CacheableInterface extends CacheableDependencyInterface {
 
   /**
    * The cache keys associated with this potentially cacheable object.
@@ -32,39 +32,6 @@ interface CacheableInterface {
    */
   public function getCacheKeys();
 
-  /**
-   * The cache contexts associated with this potentially cacheable object.
-   *
-   * These identify a specific variation/representation of the object.
-   *
-   * Cache contexts are tokens: placeholders that are converted to cache keys by
-   * the @cache_contexts service. The replacement value depends on the request
-   * context (the current URL, language, and so on). They're converted before
-   * storing an object in cache.
-   *
-   * @return string[]
-   *   An array of cache context tokens, used to generate a cache ID.
-   *
-   * @see \Drupal\Core\Cache\CacheContexts::convertTokensToKeys()
-   */
-  public function getCacheContexts();
-
-  /**
-   * The cache tags associated with this potentially cacheable object.
-   *
-   * @return string[]
-   *  An array of cache tags.
-   */
-  public function getCacheTags();
-
-  /**
-   * The maximum age for which this object may be cached.
-   *
-   * @return int
-   *   The maximum time in seconds that this object may be cached.
-   */
-  public function getCacheMaxAge();
-
   /**
    * Indicates whether this object is cacheable.
    *
diff --git a/core/lib/Drupal/Core/Cache/LanguagesCacheContext.php b/core/lib/Drupal/Core/Cache/LanguagesCacheContext.php
index 2ed15177672d09ef2942321c13ad69bdf8384bea..b9b888345d014dbdfd7258d210b9f79b3a74214a 100644
--- a/core/lib/Drupal/Core/Cache/LanguagesCacheContext.php
+++ b/core/lib/Drupal/Core/Cache/LanguagesCacheContext.php
@@ -66,7 +66,8 @@ public function getContext($type = NULL) {
       return implode(',', $context_parts);
     }
     else {
-      if (!in_array($type, $this->languageManager->getLanguageTypes())) {
+      $language_types = $this->languageManager->getDefinedLanguageTypesInfo();
+      if (!isset($language_types[$type])) {
         throw new \RuntimeException(sprintf('The language type "%s" is invalid.', $type));
       }
       return $this->languageManager->getCurrentLanguage($type)->getId();
diff --git a/core/lib/Drupal/Core/Config/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php
index f97f5fd7008b3fb93b5948d0baa8a4b372ef1f2f..eee76a2ad85b211162911a3adda164b372959af6 100644
--- a/core/lib/Drupal/Core/Config/ConfigBase.php
+++ b/core/lib/Drupal/Core/Config/ConfigBase.php
@@ -9,6 +9,8 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Component\Utility\SafeMarkup;
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use \Drupal\Core\DependencyInjection\DependencySerializationTrait;
 
 /**
@@ -26,7 +28,7 @@
  * @see \Drupal\Core\Config\Config
  * @see \Drupal\Core\Theme\ThemeSettings
  */
-abstract class ConfigBase {
+abstract class ConfigBase implements CacheableDependencyInterface {
   use DependencySerializationTrait;
 
   /**
@@ -264,13 +266,24 @@ public function merge(array $data_to_merge) {
   }
 
   /**
-   * The unique cache tag associated with this configuration object.
-   *
-   * @return string[]
-   *   An array of cache tags.
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
    */
   public function getCacheTags() {
     return ['config:' . $this->name];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/Entity.php b/core/lib/Drupal/Core/Entity/Entity.php
index 08c20cd05a2d8048a112fccc8667a549755dd9bd..5a4073fcd28f4504932c6edfa6af3a5b4ffd4a59 100644
--- a/core/lib/Drupal/Core/Entity/Entity.php
+++ b/core/lib/Drupal/Core/Entity/Entity.php
@@ -426,6 +426,13 @@ public function referencedEntities() {
     return array();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return [];
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -434,6 +441,13 @@ public function getCacheTags() {
     return [$this->entityTypeId . ':' . $this->id()];
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return Cache::PERMANENT;
+  }
+
   /**
    * {@inheritdoc}
    *
diff --git a/core/lib/Drupal/Core/Entity/EntityInterface.php b/core/lib/Drupal/Core/Entity/EntityInterface.php
index fac03809b7b5844e8db1579fae28b64b7778fc48..7b5ebc723b997d7a7c10343a0f8161a5c491c322 100644
--- a/core/lib/Drupal/Core/Entity/EntityInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityInterface.php
@@ -8,13 +8,14 @@
 namespace Drupal\Core\Entity;
 
 use Drupal\Core\Access\AccessibleInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Defines a common interface for all entity objects.
  *
  * @ingroup entity_api
  */
-interface EntityInterface extends AccessibleInterface {
+interface EntityInterface extends AccessibleInterface, CacheableDependencyInterface {
 
   /**
    * Returns the entity UUID (Universally Unique Identifier).
@@ -396,14 +397,6 @@ public function toArray();
    */
   public function getTypedData();
 
-  /**
-   * The unique cache tag associated with this entity.
-   *
-   * @return string[]
-   *   An array of cache tags.
-   */
-  public function getCacheTags();
-
   /**
    * Gets the key that is used to store configuration dependencies.
    *
diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
index d12d028d6de60d813041eb169f259a7976100e3d..13a7b50625481c11549acfbbf1b35dcbd9584866 100644
--- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
+++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php
@@ -169,12 +169,16 @@ protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langco
       // Collect cache defaults for this entity.
       '#cache' => array(
         'tags' => Cache::mergeTags($this->getCacheTags(), $entity->getCacheTags()),
-        'contexts' => [
-          'user.roles',
-        ],
+        'contexts' => $entity->getCacheContexts(),
+        'max-age' => $entity->getCacheMaxAge(),
       ),
     );
 
+     // @todo Remove when https://www.drupal.org/node/2099137 lands.
+     $build['#cache']['contexts'] = Cache::mergeContexts($build['#cache']['contexts'], [
+       'user.roles',
+     ]);
+
     // Cache the rendered output if permitted by the view mode and global entity
     // type configuration.
     if ($this->isViewModeCacheable($view_mode) && !$entity->isNew() && $entity->isDefaultRevision() && $this->entityType->isRenderCacheable()) {
diff --git a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
index 0e508a20ae83890bda0db02bc80fa3c6d36a513b..c1daada49c168276741728be74214c21a9cce578 100644
--- a/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
@@ -9,7 +9,7 @@
 
 use Drupal\Component\Datetime\DateTimePlus;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -135,7 +135,7 @@ public function onRespond(FilterResponseEvent $event) {
 
     // Apply the request's access result cacheability metadata, if it has any.
     $access_result = $request->attributes->get(AccessAwareRouterInterface::ACCESS_RESULT);
-    if ($access_result instanceof CacheableInterface) {
+    if ($access_result instanceof CacheableDependencyInterface) {
       $this->updateDrupalCacheHeaders($response, $access_result);
     }
 
@@ -161,10 +161,10 @@ public function onRespond(FilterResponseEvent $event) {
   /**
    * Updates Drupal's cache headers using the route's cacheable access result.
    *
-   * @param Response $response
-   * @param CacheableInterface $cacheable_access_result
+   * @param \Symfony\Component\HttpFoundation\Response $response
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $cacheable_access_result
    */
-  protected function updateDrupalCacheHeaders(Response $response, CacheableInterface $cacheable_access_result) {
+  protected function updateDrupalCacheHeaders(Response $response, CacheableDependencyInterface $cacheable_access_result) {
     // X-Drupal-Cache-Tags
     $cache_tags = $cacheable_access_result->getCacheTags();
     if ($response->headers->has('X-Drupal-Cache-Tags')) {
diff --git a/core/lib/Drupal/Core/Render/BubbleableMetadata.php b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
index d822d82bd944d3559bc7e2c8bbe02dafb051c76c..c4852a99aaf74de9bef01c94462051834441226d 100644
--- a/core/lib/Drupal/Core/Render/BubbleableMetadata.php
+++ b/core/lib/Drupal/Core/Render/BubbleableMetadata.php
@@ -9,13 +9,14 @@
 
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 
 /**
  * Value object used for bubbleable rendering metadata.
  *
  * @see \Drupal\Core\Render\RendererInterface::render()
  */
-class BubbleableMetadata {
+class BubbleableMetadata implements CacheableDependencyInterface {
 
   /**
    * Cache contexts.
@@ -107,6 +108,22 @@ public static function createFromRenderArray(array $build) {
     return $meta;
   }
 
+  /**
+   * Creates a bubbleable metadata object from a cacheable depended object.
+   *
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $object
+   *   The object whose cacheability metadata to retrieve.
+   *
+   * @return static
+   */
+  public static function createFromObject(CacheableDependencyInterface $object) {
+    $meta = new static();
+    $meta->contexts = $object->getCacheContexts();
+    $meta->tags = $object->getCacheTags();
+    $meta->maxAge = $object->getCacheMaxAge();
+    return $meta;
+  }
+
   /**
    * Gets cache tags.
    *
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php
index 9205f6018a63c1012a1b3c23a1e0d7f30c73331b..0ec8fa13c771a4d3fad662f6325e17fa804c8bc6 100644
--- a/core/lib/Drupal/Core/Render/Renderer.php
+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -10,6 +10,7 @@
 use Drupal\Component\Utility\Crypt;
 use Drupal\Component\Utility\NestedArray;
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Cache\CacheContexts;
 use Drupal\Core\Cache\CacheFactoryInterface;
 use Drupal\Core\Controller\ControllerResolverInterface;
@@ -788,6 +789,15 @@ public static function mergeBubbleableMetadata(array $a, array $b) {
     return $a;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function addDependency(array &$elements, CacheableDependencyInterface $dependency) {
+    $meta_a = BubbleableMetadata::createFromRenderArray($elements);
+    $meta_b = BubbleableMetadata::createFromObject($dependency);
+    $meta_a->merge($meta_b)->applyTo($elements);
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Render/RendererInterface.php b/core/lib/Drupal/Core/Render/RendererInterface.php
index 21b223d6fdffc6b76a321544af96c99a182da484..85035bfa526f79fe0c4eec74207119624254a740 100644
--- a/core/lib/Drupal/Core/Render/RendererInterface.php
+++ b/core/lib/Drupal/Core/Render/RendererInterface.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\Core\Render;
 
+use Drupal\Core\Cache\CacheableDependencyInterface;
+
 /**
  * Defines an interface for turning a render array into a string.
  */
@@ -340,6 +342,20 @@ public function getCacheableRenderArray(array $elements);
    */
   public static function mergeBubbleableMetadata(array $a, array $b);
 
+  /**
+   * Adds a dependency on an object: merges its cacheability metadata.
+   *
+   * E.g. when a render array depends on some configuration, an entity, or an
+   * access result, we must make sure their cacheability metadata is present on
+   * the render array. This method makes doing that simple.
+   *
+   * @param array &$elements
+   *   The render array to update.
+   * @param \Drupal\Core\Cache\CacheableDependencyInterface $dependency
+   *   The dependency.
+   */
+  public function addDependency(array &$elements, CacheableDependencyInterface $dependency);
+
   /**
    * Merges two attachments arrays (which live under the '#attached' key).
    *
diff --git a/core/modules/block/src/BlockAccessControlHandler.php b/core/modules/block/src/BlockAccessControlHandler.php
index 6ec53f67eab36f26b3ce48323f1a10292e92fec3..148e3e3c109410f3fcbd42a6fa45697af161bc61 100644
--- a/core/modules/block/src/BlockAccessControlHandler.php
+++ b/core/modules/block/src/BlockAccessControlHandler.php
@@ -93,7 +93,7 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
             $this->contextHandler->applyContextMapping($condition, $contexts);
           }
           catch (ContextException $e) {
-            return AccessResult::forbidden()->setCacheable(FALSE);
+            return AccessResult::forbidden()->setCacheMaxAge(0);
           }
         }
         $conditions[$condition_id] = $condition;
@@ -111,7 +111,7 @@ protected function checkAccess(EntityInterface $entity, $operation, $langcode, A
       // result should be varied.
       // @todo Change this to use $access->cacheUntilEntityChanges($entity) once
       //   https://www.drupal.org/node/2375695 is resolved.
-      return $access->setCacheable(FALSE);
+      return $access->setCacheMaxAge(0);
     }
   }
 
diff --git a/core/modules/book/book.services.yml b/core/modules/book/book.services.yml
index 89b25fc02af1b0b10cdbfd74f8d0a3237d645ce6..6d024f73bc9c1d90cbc36823fd6a5de693ae8783 100644
--- a/core/modules/book/book.services.yml
+++ b/core/modules/book/book.services.yml
@@ -6,7 +6,7 @@ services:
       - { name: breadcrumb_builder, priority: 701 }
   book.manager:
     class: Drupal\book\BookManager
-    arguments: ['@entity.manager', '@string_translation', '@config.factory', '@book.outline_storage']
+    arguments: ['@entity.manager', '@string_translation', '@config.factory', '@book.outline_storage', '@renderer']
   book.outline:
     class: Drupal\book\BookOutline
     arguments: ['@book.manager']
diff --git a/core/modules/book/src/BookManager.php b/core/modules/book/src/BookManager.php
index 26c02fa781d58dd57e690474ec90beb19d6dea58..41daf56383155cccaea5c0bc9afaf56f101e400c 100644
--- a/core/modules/book/src/BookManager.php
+++ b/core/modules/book/src/BookManager.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Cache\Cache;
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\StringTranslation\TranslationInterface;
 use Drupal\Core\StringTranslation\StringTranslationTrait;
@@ -63,14 +64,22 @@ class BookManager implements BookManagerInterface {
    */
   protected $bookTreeFlattened;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a BookManager object.
    */
-  public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage) {
+  public function __construct(EntityManagerInterface $entity_manager, TranslationInterface $translation, ConfigFactoryInterface $config_factory, BookOutlineStorageInterface $book_outline_storage, RendererInterface $renderer) {
     $this->entityManager = $entity_manager;
     $this->stringTranslation = $translation;
     $this->configFactory = $config_factory;
     $this->bookOutlineStorage = $book_outline_storage;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -353,7 +362,7 @@ protected function addParentSelectFormElements(array $book_link) {
         '#suffix' => '</div>',
       );
     }
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    $this->renderer->addDependency($form, $config);
 
     return $form;
   }
diff --git a/core/modules/book/tests/src/Unit/BookManagerTest.php b/core/modules/book/tests/src/Unit/BookManagerTest.php
index 2c12385c16d88c9c0e05006daa41c9b2f595824a..cb45b2f0f818ce3f2bfec054a392f980c616b1f4 100644
--- a/core/modules/book/tests/src/Unit/BookManagerTest.php
+++ b/core/modules/book/tests/src/Unit/BookManagerTest.php
@@ -37,6 +37,13 @@ class BookManagerTest extends UnitTestCase {
    */
   protected $translation;
 
+  /**
+   * The mocked renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface|\PHPUnit_Framework_MockObject_MockObject
+   */
+  protected $renderer;
+
   /**
    * The tested book manager.
    *
@@ -59,7 +66,8 @@ protected function setUp() {
     $this->translation = $this->getStringTranslationStub();
     $this->configFactory = $this->getConfigFactoryStub(array());
     $this->bookOutlineStorage = $this->getMock('Drupal\book\BookOutlineStorageInterface');
-    $this->bookManager = new BookManager($this->entityManager, $this->translation, $this->configFactory, $this->bookOutlineStorage);
+    $this->renderer = $this->getMock('\Drupal\Core\Render\RendererInterface');
+    $this->bookManager = new BookManager($this->entityManager, $this->translation, $this->configFactory, $this->bookOutlineStorage, $this->renderer);
   }
 
   /**
diff --git a/core/modules/comment/src/CommentForm.php b/core/modules/comment/src/CommentForm.php
index 903bafae3e8659c9e925145014c81ff42d8d4256..65eea3e1c2e8bba690bb872c518f10dc50c6e57d 100644
--- a/core/modules/comment/src/CommentForm.php
+++ b/core/modules/comment/src/CommentForm.php
@@ -16,6 +16,7 @@
 use Drupal\Core\Entity\EntityManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
@@ -31,13 +32,21 @@ class CommentForm extends ContentEntityForm {
    */
   protected $currentUser;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * {@inheritdoc}
    */
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('entity.manager'),
-      $container->get('current_user')
+      $container->get('current_user'),
+      $container->get('renderer')
     );
   }
 
@@ -48,10 +57,13 @@ public static function create(ContainerInterface $container) {
    *   The entity manager service.
    * @param \Drupal\Core\Session\AccountInterface $current_user
    *   The current user.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
    */
-  public function __construct(EntityManagerInterface $entity_manager, AccountInterface $current_user) {
+  public function __construct(EntityManagerInterface $entity_manager, AccountInterface $current_user, RendererInterface $renderer) {
     parent::__construct($entity_manager);
     $this->currentUser = $current_user;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -213,12 +225,9 @@ public function form(array $form, FormStateInterface $form_state) {
       '#access' => $is_admin,
     );
 
-    $form['#cache']['tags'] = Cache::mergeTags(
-      isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],
-      $config->getCacheTags(),
-      // The form depends on the field definition.
-      $field_definition->getConfig($entity->bundle())->getCacheTags()
-    );
+    $this->renderer->addDependency($form, $config);
+    // The form depends on the field definition.
+    $this->renderer->addDependency($form, $field_definition->getConfig($entity->bundle()));
 
     return parent::form($form, $form_state, $comment);
   }
diff --git a/core/modules/contact/src/Controller/ContactController.php b/core/modules/contact/src/Controller/ContactController.php
index f3d6825daf1a3224c52a61cad245695d495182e5..df545297b3851c7d124085c97331e6f8e9a4fdfb 100644
--- a/core/modules/contact/src/Controller/ContactController.php
+++ b/core/modules/contact/src/Controller/ContactController.php
@@ -12,6 +12,7 @@
 use Drupal\Core\Datetime\DateFormatter;
 use Drupal\Core\Flood\FloodInterface;
 use Drupal\contact\ContactFormInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\user\UserInterface;
 use Drupal\Component\Utility\SafeMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -37,6 +38,13 @@ class ContactController extends ControllerBase {
    */
   protected $dateFormatter;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a ContactController object.
    *
@@ -44,10 +52,13 @@ class ContactController extends ControllerBase {
    *   The flood service.
    * @param \Drupal\Core\Datetime\DateFormatter $date_formatter
    *   The date service.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
    */
-  public function __construct(FloodInterface $flood, DateFormatter $date_formatter) {
+  public function __construct(FloodInterface $flood, DateFormatter $date_formatter, RendererInterface $renderer) {
     $this->flood = $flood;
     $this->dateFormatter = $date_formatter;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -56,7 +67,8 @@ public function __construct(FloodInterface $flood, DateFormatter $date_formatter
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('flood'),
-      $container->get('date.formatter')
+      $container->get('date.formatter'),
+      $container->get('renderer')
     );
   }
 
@@ -106,7 +118,7 @@ public function contactSitePage(ContactFormInterface $contact_form = NULL) {
 
     $form = $this->entityFormBuilder()->getForm($message);
     $form['#title'] = SafeMarkup::checkPlain($contact_form->label());
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    $this->renderer->addDependency($form, $config);
     return $form;
   }
 
diff --git a/core/modules/forum/src/Controller/ForumController.php b/core/modules/forum/src/Controller/ForumController.php
index 18d7325ef627d24718f57c84330d065637eca466..82165fff8e852ccbf5c89606af086d619b725dff 100644
--- a/core/modules/forum/src/Controller/ForumController.php
+++ b/core/modules/forum/src/Controller/ForumController.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Controller\ControllerBase;
 use Drupal\Core\Entity\EntityAccessControlHandlerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Url;
 use Drupal\forum\ForumManagerInterface;
@@ -66,6 +67,13 @@ class ForumController extends ControllerBase {
    */
   protected $nodeTypeStorage;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a ForumController object.
    *
@@ -83,8 +91,10 @@ class ForumController extends ControllerBase {
    *   Array of active fields on the site.
    * @param \Drupal\Core\Entity\EntityStorageInterface $node_type_storage
    *   Node type storage handler.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
    */
-  public function __construct(ForumManagerInterface $forum_manager, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user, EntityAccessControlHandlerInterface $node_access, array $field_map, EntityStorageInterface $node_type_storage) {
+  public function __construct(ForumManagerInterface $forum_manager, VocabularyStorageInterface $vocabulary_storage, TermStorageInterface $term_storage, AccountInterface $current_user, EntityAccessControlHandlerInterface $node_access, array $field_map, EntityStorageInterface $node_type_storage, RendererInterface $renderer) {
     $this->forumManager = $forum_manager;
     $this->vocabularyStorage = $vocabulary_storage;
     $this->termStorage = $term_storage;
@@ -92,6 +102,7 @@ public function __construct(ForumManagerInterface $forum_manager, VocabularyStor
     $this->nodeAccess = $node_access;
     $this->fieldMap = $field_map;
     $this->nodeTypeStorage = $node_type_storage;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -107,7 +118,8 @@ public static function create(ContainerInterface $container) {
       $container->get('current_user'),
       $entity_manager->getAccessControlHandler('node'),
       $entity_manager->getFieldMap(),
-      $entity_manager->getStorage('node_type')
+      $entity_manager->getStorage('node_type'),
+      $container->get('renderer')
     );
   }
 
@@ -191,7 +203,7 @@ protected function build($forums, TermInterface $term, $topics = array(), $paren
     if (empty($term->forum_container->value)) {
       $build['#attached']['feed'][] = array('taxonomy/term/' . $term->id() . '/feed', 'RSS - ' . $term->getName());
     }
-    $build['#cache']['tags'] = Cache::mergeTags(isset($build['#cache']['tags']) ? $build['#cache']['tags'] : [],  $config->getCacheTags());
+    $this->renderer->addDependency($build, $config);
 
     return [
       'action' => $this->buildActionLinks($config->get('vocabulary'), $term),
diff --git a/core/modules/menu_ui/src/Form/MenuLinkResetForm.php b/core/modules/menu_ui/src/Form/MenuLinkResetForm.php
index 9c4c63b59df8fd19c7eee008877e77ef793465c0..fd0be68125f34a72f57270963b7bc457c58272f0 100644
--- a/core/modules/menu_ui/src/Form/MenuLinkResetForm.php
+++ b/core/modules/menu_ui/src/Form/MenuLinkResetForm.php
@@ -119,7 +119,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
    *   The access result.
    */
   public function linkIsResettable(MenuLinkInterface $menu_link_plugin) {
-    return AccessResult::allowedIf($menu_link_plugin->isResettable())->setCacheable(FALSE);
+    return AccessResult::allowedIf($menu_link_plugin->isResettable())->setCacheMaxAge(0);
   }
 
 }
diff --git a/core/modules/node/src/NodeGrantDatabaseStorage.php b/core/modules/node/src/NodeGrantDatabaseStorage.php
index 5c661900468560404cb22c3bcec6149eec2aae16..218524551db2cea02e17bb9b532dd08310e33c0c 100644
--- a/core/modules/node/src/NodeGrantDatabaseStorage.php
+++ b/core/modules/node/src/NodeGrantDatabaseStorage.php
@@ -108,7 +108,7 @@ public function access(NodeInterface $node, $operation, $langcode, AccountInterf
     $set_cacheability = function (AccessResult $access_result) use ($operation) {
       $access_result->addCacheContexts(['user.node_grants:' . $operation]);
       if ($operation !== 'view') {
-        $access_result->setCacheable(FALSE);
+        $access_result->setCacheMaxAge(0);
       }
       return $access_result;
     };
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index b44117ecdd0c0927aad6c2a7e4f59ad7185b9e2e..e8d1d3624adee3b4301d1cab71ed7785d056316a 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -148,8 +148,8 @@ function node_access_test_node_access(\Drupal\node\NodeInterface $node, $op, \Dr
   $secret_catalan = \Drupal::state()->get('node_access_test_secret_catalan') ?: 0;
   if ($secret_catalan && $langcode == 'ca') {
     // Make all Catalan content secret.
-    return AccessResult::forbidden()->setCacheable(FALSE);
+    return AccessResult::forbidden()->setCacheMaxAge(0);
   }
   // No opinion.
-  return AccessResult::neutral()->setCacheable(FALSE);
+  return AccessResult::neutral()->setCacheMaxAge(0);
 }
diff --git a/core/modules/rest/src/Access/CSRFAccessCheck.php b/core/modules/rest/src/Access/CSRFAccessCheck.php
index 2e39c61d1b49de80a09330cb2412e967b8e2b45d..5db08fe7a7a394232a0284180101126025c5a1c1 100644
--- a/core/modules/rest/src/Access/CSRFAccessCheck.php
+++ b/core/modules/rest/src/Access/CSRFAccessCheck.php
@@ -83,11 +83,11 @@ public function access(Request $request, AccountInterface $account) {
     ) {
       $csrf_token = $request->headers->get('X-CSRF-Token');
       if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) {
-        return AccessResult::forbidden()->setCacheable(FALSE);
+        return AccessResult::forbidden()->setCacheMaxAge(0);
       }
     }
     // Let other access checkers decide if the request is legit.
-    return AccessResult::allowed()->setCacheable(FALSE);
+    return AccessResult::allowed()->setCacheMaxAge(0);
   }
 
 }
diff --git a/core/modules/system/src/Access/CronAccessCheck.php b/core/modules/system/src/Access/CronAccessCheck.php
index acb1401bea92350131ba65545c16f6d0b155f368..fa555da17b174283d4c281cafce8a4644d106ff6 100644
--- a/core/modules/system/src/Access/CronAccessCheck.php
+++ b/core/modules/system/src/Access/CronAccessCheck.php
@@ -27,12 +27,12 @@ class CronAccessCheck implements AccessInterface {
   public function access($key) {
     if ($key != \Drupal::state()->get('system.cron_key')) {
       \Drupal::logger('cron')->notice('Cron could not run because an invalid key was used.');
-      return AccessResult::forbidden()->setCacheable(FALSE);
+      return AccessResult::forbidden()->setCacheMaxAge(0);
     }
     elseif (\Drupal::state()->get('system.maintenance_mode')) {
       \Drupal::logger('cron')->notice('Cron could not run because the site is in maintenance mode.');
-      return AccessResult::forbidden()->setCacheable(FALSE);
+      return AccessResult::forbidden()->setCacheMaxAge(0);
     }
-    return AccessResult::allowed()->setCacheable(FALSE);
+    return AccessResult::allowed()->setCacheMaxAge(0);
   }
 }
diff --git a/core/modules/system/src/Access/DbUpdateAccessCheck.php b/core/modules/system/src/Access/DbUpdateAccessCheck.php
index 32e7ccf635c3e86fc4a1f57cff6f7af06d0c60ab..f78199185141adbb630bde77c087bfad72380c57 100644
--- a/core/modules/system/src/Access/DbUpdateAccessCheck.php
+++ b/core/modules/system/src/Access/DbUpdateAccessCheck.php
@@ -29,7 +29,7 @@ class DbUpdateAccessCheck implements AccessInterface {
   public function access(AccountInterface $account) {
     // Allow the global variable in settings.php to override the access check.
     if (Settings::get('update_free_access')) {
-      return AccessResult::allowed()->setCacheable(FALSE);
+      return AccessResult::allowed()->setCacheMaxAge(0);
     }
 
     if ($account->hasPermission('administer software updates')) {
diff --git a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
index 6e87d2dff28804d71f2aa0edc5b16a0b9c0f0bd7..e44299bab491945290aec2ea943d22f0bad91b2a 100644
--- a/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
+++ b/core/modules/system/src/Tests/Entity/EntityViewBuilderTest.php
@@ -58,7 +58,7 @@ public function testEntityViewBuilderCache() {
     // Test that new entities (before they are saved for the first time) do not
     // generate a cache entry.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'The render array element of new (unsaved) entities is not cached, but does have cache tags set.');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'The render array element of new (unsaved) entities is not cached, but does have cache tags set.');
 
     // Get a fully built entity view render array.
     $entity_test->save();
@@ -163,17 +163,17 @@ public function testEntityViewBuilderCacheToggling() {
     // Test a view mode in default conditions: render caching is enabled for
     // the entity type and the view mode.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'keys', 'bin'] , 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts and bin).');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age', 'keys', 'bin'] , 'A view mode with render cache enabled has the correct output (cache tags, keys, contexts, max-age and bin).');
 
     // Test that a view mode can opt out of render caching.
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test')->view($entity_test, 'test');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'A view mode with render cache disabled has the correct output (only cache tags).');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'A view mode with render cache disabled has the correct output (only cache tags, contexts and max-age).');
 
     // Test that an entity type can opt out of render caching completely.
     $entity_test_no_cache = $this->createTestEntity('entity_test_label');
     $entity_test_no_cache->save();
     $build = $this->container->get('entity.manager')->getViewBuilder('entity_test_label')->view($entity_test_no_cache, 'full');
-    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts'], 'An entity type can opt out of render caching regardless of view mode configuration, but always has cache tags set.');
+    $this->assertTrue(isset($build['#cache']) && array_keys($build['#cache']) == ['tags', 'contexts', 'max-age'], 'An entity type can opt out of render caching regardless of view mode configuration, but always has cache tags, contexts and max-age set.');
   }
 
   /**
diff --git a/core/modules/system/tests/modules/entity_test/entity_test.module b/core/modules/system/tests/modules/entity_test/entity_test.module
index df80c1a9ddbea4c19902d330d384db91ac87fa36..174529424b0a9f03fe8186208b578d8d61d445e8 100644
--- a/core/modules/system/tests/modules/entity_test/entity_test.module
+++ b/core/modules/system/tests/modules/entity_test/entity_test.module
@@ -552,7 +552,7 @@ function entity_test_entity_access(EntityInterface $entity, $operation, AccountI
   // Uncacheable because the access result depends on a State key-value pair and
   // might therefore change at any time.
   $condition = \Drupal::state()->get("entity_test_entity_access.{$operation}." . $entity->id(), FALSE);
-  return AccessResult::allowedIf($condition)->setCacheable(FALSE);
+  return AccessResult::allowedIf($condition)->setCacheMaxAge(0);
 }
 
 /**
diff --git a/core/modules/update/src/Access/UpdateManagerAccessCheck.php b/core/modules/update/src/Access/UpdateManagerAccessCheck.php
index d057ce3290bd0557df2fa0852a567910e53c77bf..df52c5c37314cd64b0418559c83d6b0bc39639c0 100644
--- a/core/modules/update/src/Access/UpdateManagerAccessCheck.php
+++ b/core/modules/update/src/Access/UpdateManagerAccessCheck.php
@@ -42,7 +42,7 @@ public function __construct(Settings $settings) {
   public function access() {
     // Uncacheable because the access result depends on a Settings key-value
     // pair, and can therefore change at any time.
-    return AccessResult::allowedIf($this->settings->get('allow_authorize_operations', TRUE))->setCacheable(FALSE);
+    return AccessResult::allowedIf($this->settings->get('allow_authorize_operations', TRUE))->setCacheMaxAge(0);
   }
 
 }
diff --git a/core/modules/user/src/Form/UserLoginForm.php b/core/modules/user/src/Form/UserLoginForm.php
index a0c08768d8af4bb2eb1c6fef585f50d9cfec0b62..76736122f300f6946bb148a60247e69a20f7b5ef 100644
--- a/core/modules/user/src/Form/UserLoginForm.php
+++ b/core/modules/user/src/Form/UserLoginForm.php
@@ -11,6 +11,7 @@
 use Drupal\Core\Flood\FloodInterface;
 use Drupal\Core\Form\FormBase;
 use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
 use Drupal\user\UserAuthInterface;
 use Drupal\user\UserStorageInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -41,6 +42,13 @@ class UserLoginForm extends FormBase {
    */
   protected $userAuth;
 
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
   /**
    * Constructs a new UserLoginForm.
    *
@@ -50,11 +58,14 @@ class UserLoginForm extends FormBase {
    *   The user storage.
    * @param \Drupal\user\UserAuthInterface $user_auth
    *   The user authentication object.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
    */
-  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, UserAuthInterface $user_auth) {
+  public function __construct(FloodInterface $flood, UserStorageInterface $user_storage, UserAuthInterface $user_auth, RendererInterface $renderer) {
     $this->flood = $flood;
     $this->userStorage = $user_storage;
     $this->userAuth = $user_auth;
+    $this->renderer = $renderer;
   }
 
   /**
@@ -64,7 +75,8 @@ public static function create(ContainerInterface $container) {
     return new static(
       $container->get('flood'),
       $container->get('entity.manager')->getStorage('user'),
-      $container->get('user.auth')
+      $container->get('user.auth'),
+      $container->get('renderer')
     );
   }
 
@@ -112,7 +124,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     $form['#validate'][] = '::validateAuthentication';
     $form['#validate'][] = '::validateFinal';
 
-    $form['#cache']['tags'] = Cache::mergeTags(isset($form['#cache']['tags']) ? $form['#cache']['tags'] : [],  $config->getCacheTags());
+    $this->renderer->addDependency($form, $config);
 
     return $form;
   }
diff --git a/core/modules/views_ui/src/ViewUI.php b/core/modules/views_ui/src/ViewUI.php
index 5aeac875cfa2357fc8ee2fefe87da3dc692d056b..31d29b38278bdaaa8490b6eba958442321d6fc1f 100644
--- a/core/modules/views_ui/src/ViewUI.php
+++ b/core/modules/views_ui/src/ViewUI.php
@@ -1208,6 +1208,13 @@ public function getDependencies() {
     return $this->storage->getDependencies();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->storage->getCacheContexts();
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -1215,6 +1222,13 @@ public function getCacheTags() {
     return $this->storage->getCacheTags();
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->storage->getCacheMaxAge();
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
index 85028fdd7096a898d831e4a6d8b5e2a1a1e9a1b0..c549a3c373149c6d9aefd65f5798411ddacd4f76 100644
--- a/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/AccessResultTest.php
@@ -11,7 +11,7 @@
 use Drupal\Core\Access\AccessResultInterface;
 use Drupal\Core\Access\AccessResultNeutral;
 use Drupal\Core\Cache\Cache;
-use Drupal\Core\Cache\CacheableInterface;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Tests\UnitTestCase;
 
 /**
@@ -21,11 +21,8 @@
 class AccessResultTest extends UnitTestCase {
 
   protected function assertDefaultCacheability(AccessResult $access) {
-    $this->assertTrue($access->isCacheable());
-    $this->assertSame([], $access->getCacheKeys());
     $this->assertSame([], $access->getCacheContexts());
     $this->assertSame([], $access->getCacheTags());
-    $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
   }
 
@@ -34,7 +31,6 @@ protected function assertDefaultCacheability(AccessResult $access) {
    *
    * @covers ::__construct
    * @covers ::neutral
-   * @covers ::getCacheBin
    */
   public function testConstruction() {
     $verify = function (AccessResult $access) {
@@ -297,16 +293,6 @@ public function testOrIf() {
     $this->assertDefaultCacheability($access);
   }
 
-  /**
-   * @covers ::setCacheable
-   * @covers ::isCacheable
-   */
-  public function testCacheable() {
-    $this->assertTrue(AccessResult::neutral()->isCacheable());
-    $this->assertTrue(AccessResult::neutral()->setCacheable(TRUE)->isCacheable());
-    $this->assertFalse(AccessResult::neutral()->setCacheable(FALSE)->isCacheable());
-  }
-
   /**
    * @covers ::setCacheMaxAge
    * @covers ::getCacheMaxAge
@@ -329,8 +315,6 @@ public function testCacheContexts() {
       $this->assertFalse($access->isAllowed());
       $this->assertFalse($access->isForbidden());
       $this->assertTrue($access->isNeutral());
-      $this->assertTrue($access->isCacheable());
-      $this->assertSame('default', $access->getCacheBin());
       $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
       $this->assertSame($contexts, $access->getCacheContexts());
       $this->assertSame([], $access->getCacheTags());
@@ -408,8 +392,6 @@ public function testCacheTags() {
       $this->assertFalse($access->isAllowed());
       $this->assertFalse($access->isForbidden());
       $this->assertTrue($access->isNeutral());
-      $this->assertTrue($access->isCacheable());
-      $this->assertSame('default', $access->getCacheBin());
       $this->assertSame(Cache::PERMANENT, $access->getCacheMaxAge());
       $this->assertSame([], $access->getCacheContexts());
       $this->assertSame($tags, $access->getCacheTags());
@@ -471,20 +453,16 @@ public function testInheritCacheability() {
     $access = AccessResult::allowed();
     $other = AccessResult::allowed()->setCacheMaxAge(1500)->cachePerPermissions()->addCacheTags(['node:20011988']);
     $this->assertTrue($access->inheritCacheability($other) instanceof AccessResult);
-    $this->assertTrue($access->isCacheable());
     $this->assertSame(['user.permissions'], $access->getCacheContexts());
     $this->assertSame(['node:20011988'], $access->getCacheTags());
-    $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(1500, $access->getCacheMaxAge());
 
     // andIf(); 1st has custom tags, max-age, 2nd has custom contexts and max-age.
     $access = AccessResult::allowed()->cachePerUser()->setCacheMaxAge(43200);
     $other = AccessResult::forbidden()->addCacheTags(['node:14031991'])->setCacheMaxAge(86400);
     $this->assertTrue($access->inheritCacheability($other) instanceof AccessResult);
-    $this->assertTrue($access->isCacheable());
     $this->assertSame(['user'], $access->getCacheContexts());
     $this->assertSame(['node:14031991'], $access->getCacheTags());
-    $this->assertSame('default', $access->getCacheBin());
     $this->assertSame(43200, $access->getCacheMaxAge());
   }
 
@@ -495,15 +473,17 @@ public function testInheritCacheability() {
    * every single bit of cacheability metadata, which would lead to a mind-
    * boggling number of permutations, in this test, we only consider the
    * permutations of all pairs of the following set:
-   * 1. Allowed, implements cacheable interface, is cacheable
-   * 2. Allowed, implements cacheable interface, is not cacheable
-   * 3. Allowed, does not implement cacheable interface (hence not cacheable)
-   * 4. Forbidden, implements cacheable interface, is cacheable
-   * 5. Forbidden, implements cacheable interface, is not cacheable
-   * 6. Forbidden, does not implement cacheable interface (hence not cacheable)
-   * 7. Neutral, implements cacheable interface, is cacheable
-   * 8. Neutral, implements cacheable interface, is not cacheable
-   * 9. Neutral, does not implement cacheable interface (hence not cacheable)
+   * 1. Allowed, implements CDI, is cacheable
+   * 2. Allowed, implements CDI, is not cacheable
+   * 3. Allowed, does not implement CDI (hence not cacheable)
+   * 4. Forbidden, implements CDI, is cacheable
+   * 5. Forbidden, implements CDI, is not cacheable
+   * 6. Forbidden, does not implement CDI (hence not cacheable)
+   * 7. Neutral, implements CDI, is cacheable
+   * 8. Neutral, implements CDI, is not cacheable
+   * 9. Neutral, does not implement CDI (hence not cacheable)
+   *
+   * (Where "CDI" is CacheableDependencyInterface.)
    *
    * This leads to 72 permutations (9!/(9-2)! = 9*8 = 72) per operation. There
    * are two operations to test (AND and OR), so that leads to a grand total of
@@ -517,8 +497,8 @@ public function testInheritCacheability() {
    * 2. Any operation yields an access result object that is of the same class
    *    (implementation) as the first operand. This is because operations are
    *    invoked on the first operand. Therefore, if the first implementation
-   *    does not implement CacheableInterface, then the result won't either.
-   *    This is the case for items 3, 6 and 9 in the set above.
+   *    does not implement CacheableDependencyInterface, then the result won't
+   *    either. This is the case for items 3, 6 and 9 in the set above.
    */
   public function andOrCacheabilityPropagationProvider() {
     // ct: cacheable=true, cf: cacheable=false, un: uncacheable.
@@ -526,23 +506,23 @@ public function andOrCacheabilityPropagationProvider() {
     // test UncacheableTestAccessResult, not AccessResult. However, we
     // definitely want to verify that AccessResult's orIf() and andIf() methods
     // work correctly when given an AccessResultInterface implementation that
-    // does not implement CacheableInterface, and we want to test the full gamut
-    // of permutations, so that's not a problem.
+    // does not implement CacheableDependencyInterface, and we want to test the
+    // full gamut of permutations, so that's not a problem.
     $allowed_ct = AccessResult::allowed();
-    $allowed_cf = AccessResult::allowed()->setCacheable(FALSE);
+    $allowed_cf = AccessResult::allowed()->setCacheMaxAge(0);
     $allowed_un = new UncacheableTestAccessResult('ALLOWED');
     $forbidden_ct = AccessResult::forbidden();
-    $forbidden_cf = AccessResult::forbidden()->setCacheable(FALSE);
+    $forbidden_cf = AccessResult::forbidden()->setCacheMaxAge(0);
     $forbidden_un = new UncacheableTestAccessResult('FORBIDDEN');
     $neutral_ct = AccessResult::neutral();
-    $neutral_cf = AccessResult::neutral()->setCacheable(FALSE);
+    $neutral_cf = AccessResult::neutral()->setCacheMaxAge(0);
     $neutral_un = new UncacheableTestAccessResult('NEUTRAL');
 
     // Structure:
     // - First column: first access result.
     // - Second column: operator ('OR' or 'AND').
     // - Third column: second access result.
-    // - Fourth column: whether the result implements CacheableInterface
+    // - Fourth column: whether result implements CacheableDependencyInterface
     // - Fifth column: whether the result is cacheable (if column 4 is TRUE)
     return [
       // Allowed (ct) OR allowed (ct,cf,un).
@@ -795,7 +775,7 @@ public function andOrCacheabilityPropagationProvider() {
    *
    * @dataProvider andOrCacheabilityPropagationProvider
    */
-  public function testAndOrCacheabilityPropagation(AccessResultInterface $first, $op, AccessResultInterface $second, $implements_cacheable_interface, $is_cacheable) {
+  public function testAndOrCacheabilityPropagation(AccessResultInterface $first, $op, AccessResultInterface $second, $implements_cacheable_dependency_interface, $is_cacheable) {
     if ($op === 'OR') {
       $result = $first->orIf($second);
     }
@@ -805,14 +785,14 @@ public function testAndOrCacheabilityPropagation(AccessResultInterface $first, $
     else {
       throw new \LogicException('Invalid operator specified');
     }
-    if ($implements_cacheable_interface) {
-      $this->assertTrue($result instanceof CacheableInterface, 'Result is an instance of CacheableInterface.');
-      if ($result instanceof CacheableInterface) {
-        $this->assertSame($is_cacheable, $result->isCacheable(), 'isCacheable() matches expectations.');
+    if ($implements_cacheable_dependency_interface) {
+      $this->assertTrue($result instanceof CacheableDependencyInterface, 'Result is an instance of CacheableDependencyInterface.');
+      if ($result instanceof CacheableDependencyInterface) {
+        $this->assertSame($is_cacheable, $result->getCacheMaxAge() !== 0, 'getCacheMaxAge() matches expectations.');
       }
     }
     else {
-      $this->assertFalse($result instanceof CacheableInterface, 'Result is not an instance of CacheableInterface.');
+      $this->assertFalse($result instanceof CacheableDependencyInterface, 'Result is not an instance of CacheableDependencyInterface.');
     }
   }
 
diff --git a/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php
index 023f0e658821b22a38b9c23d51a777fb35401747..b9104e2e9c9104c45b3a534b8d285734541d3e3d 100644
--- a/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php
+++ b/core/tests/Drupal/Tests/Core/Access/CsrfAccessCheckTest.php
@@ -66,7 +66,7 @@ public function testAccessTokenPass() {
     $route = new Route('/test-path/{node}', array(), array('_csrf_token' => 'TRUE'));
     $request = Request::create('/test-path/42?token=test_query');
 
-    $this->assertEquals(AccessResult::allowed()->setCacheable(FALSE), $this->accessCheck->access($route, $request, $this->routeMatch));
+    $this->assertEquals(AccessResult::allowed()->setCacheMaxAge(0), $this->accessCheck->access($route, $request, $this->routeMatch));
   }
 
   /**
@@ -85,7 +85,7 @@ public function testAccessTokenFail() {
     $route = new Route('/test-path', array(), array('_csrf_token' => 'TRUE'));
     $request = Request::create('/test-path?token=test_query');
 
-    $this->assertEquals(AccessResult::forbidden()->setCacheable(FALSE), $this->accessCheck->access($route, $request, $this->routeMatch));
+    $this->assertEquals(AccessResult::forbidden()->setCacheMaxAge(0), $this->accessCheck->access($route, $request, $this->routeMatch));
   }
 
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
index f6c4cfb5db70714528b0c33efeb8783092d398ff..526b7ee899e26126b862981dd8efc0dec8c61b88 100644
--- a/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/BubbleableMetadataTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Render;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Render\BubbleableMetadata;
 use Drupal\Tests\UnitTestCase;
 use Drupal\Core\Render\Element;
@@ -131,4 +132,35 @@ public function providerTestCreateFromRenderArray() {
     return $data;
   }
 
+  /**
+   * @covers ::createFromObject
+   * @dataProvider providerTestCreateFromObject
+   */
+  public function testCreateFromObject(CacheableDependencyInterface $object, BubbleableMetadata $expected) {
+    $this->assertEquals($expected, BubbleableMetadata::createFromObject($object));
+  }
+
+  /**
+   * Provides test data for createFromObject().
+   *
+   * @return array
+   */
+  public function providerTestCreateFromObject() {
+    $data = [];
+
+    $empty_metadata = new BubbleableMetadata();
+    $nonempty_metadata = new BubbleableMetadata();
+    $nonempty_metadata->setCacheContexts(['qux'])
+      ->setCacheTags(['foo:bar'])
+      ->setCacheMaxAge(600);
+
+    $empty_cacheable_object = new TestCacheableDependency([], [], Cache::PERMANENT);
+    $nonempty_cacheable_object = new TestCacheableDependency(['qux'], ['foo:bar'], 600);
+
+    $data[] = [$empty_cacheable_object, $empty_metadata];
+    $data[] = [$nonempty_cacheable_object, $nonempty_metadata];
+
+    return $data;
+  }
+
 }
diff --git a/core/tests/Drupal/Tests/Core/Render/RendererTest.php b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
index 7ac67b1b4b90d5a36f3750dde8f74a161b85edbb..fb757f7dfd1a2b8db5d39e6fc65f782dd5672a93 100644
--- a/core/tests/Drupal/Tests/Core/Render/RendererTest.php
+++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
@@ -8,6 +8,7 @@
 namespace Drupal\Tests\Core\Render;
 
 use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\CacheableDependencyInterface;
 use Drupal\Core\Render\Element;
 use Drupal\Core\Template\Attribute;
 
@@ -620,6 +621,69 @@ public function providerTestRenderCacheMaxAge() {
     ];
   }
 
+  /**
+   * @covers ::addDependency
+   *
+   * @dataProvider providerTestAddDependency
+   */
+  public function testAddDependency(array $build, CacheableDependencyInterface $object, array $expected) {
+    $this->renderer->addDependency($build, $object);
+    $this->assertEquals($build, $expected);
+  }
+
+  public function providerTestAddDependency() {
+    return [
+      // Empty render array, typical default cacheability.
+      [
+        [],
+        new TestCacheableDependency([], [], Cache::PERMANENT),
+        [
+          '#cache' => [
+            'contexts' => [],
+            'tags' => [],
+            'max-age' => Cache::PERMANENT,
+          ],
+          '#attached' => [],
+          '#post_render_cache' => [],
+        ],
+      ],
+      // Empty render array, some cacheability.
+      [
+        [],
+        new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT),
+        [
+          '#cache' => [
+            'contexts' => ['user.roles'],
+            'tags' => ['foo'],
+            'max-age' => Cache::PERMANENT,
+          ],
+          '#attached' => [],
+          '#post_render_cache' => [],
+        ],
+      ],
+      // Cacheable render array, some cacheability.
+      [
+        [
+          '#cache' => [
+            'contexts' => ['theme'],
+            'tags' => ['bar'],
+            'max-age' => 600,
+          ]
+        ],
+        new TestCacheableDependency(['user.roles'], ['foo'], Cache::PERMANENT),
+        [
+          '#cache' => [
+            'contexts' => ['theme', 'user.roles'],
+            'tags' => ['bar', 'foo'],
+            'max-age' => 600,
+          ],
+          '#attached' => [],
+          '#post_render_cache' => [],
+        ],
+      ],
+    ];
+  }
+
 }
 
 class TestAccessClass {
diff --git a/core/tests/Drupal/Tests/Core/Render/TestCacheableDependency.php b/core/tests/Drupal/Tests/Core/Render/TestCacheableDependency.php
new file mode 100644
index 0000000000000000000000000000000000000000..8980e8934042df9c51845b0d3a919be1c648d6ad
--- /dev/null
+++ b/core/tests/Drupal/Tests/Core/Render/TestCacheableDependency.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\Tests\Core\Render;
+
+use Drupal\Core\Cache\CacheableDependencyInterface;
+
+/**
+ * Cacheable dependency object for use in tests.
+ */
+class TestCacheableDependency implements CacheableDependencyInterface {
+
+  public function __construct(array $contexts, array $tags, $max_age) {
+    $this->contexts = $contexts;
+    $this->tags = $tags;
+    $this->maxAge = $max_age;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheContexts() {
+    return $this->contexts;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheTags() {
+    return $this->tags;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCacheMaxAge() {
+    return $this->maxAge;
+  }
+
+}