diff --git a/core/core.services.yml b/core/core.services.yml
index 76088786cd..a7997ea66b 100644
--- a/core/core.services.yml
+++ b/core/core.services.yml
@@ -1152,7 +1152,7 @@ services:
     arguments: ['@state', '@current_user']
   maintenance_mode_subscriber:
     class: Drupal\Core\EventSubscriber\MaintenanceModeSubscriber
-    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer']
+    arguments: ['@maintenance_mode', '@config.factory', '@string_translation', '@url_generator', '@current_user', '@bare_html_page_renderer', '@messenger']
     tags:
       - { name: event_subscriber }
   path_subscriber:
@@ -1660,3 +1660,6 @@ services:
     class: Drupal\Core\EventSubscriber\RssResponseRelativeUrlFilter
     tags:
       - { name: event_subscriber }
+  messenger:
+    class: Drupal\Core\Messenger\LegacyMessenger
+    arguments: ['@page_cache_kill_switch']
diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc
index 52f574212a..6d985be913 100644
--- a/core/includes/bootstrap.inc
+++ b/core/includes/bootstrap.inc
@@ -11,8 +11,7 @@
 use Drupal\Component\Utility\Unicode;
 use Drupal\Core\Config\BootstrapConfigStorageFactory;
 use Drupal\Core\Logger\RfcLogLevel;
-use Drupal\Core\Render\Markup;
-use Drupal\Component\Render\MarkupInterface;
+use Drupal\Core\Messenger\LegacyMessenger;
 use Drupal\Core\Test\TestDatabase;
 use Drupal\Core\Session\AccountInterface;
 use Drupal\Core\Site\Settings;
@@ -457,30 +456,15 @@ function watchdog_exception($type, Exception $exception, $message = NULL, $varia
  *
  * @see drupal_get_messages()
  * @see status-messages.html.twig
+ *
+ * @deprecated in Drupal 8.5.0, will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('messenger')->addMessage() instead.
  */
 function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE) {
-  if (isset($message)) {
-    if (!isset($_SESSION['messages'][$type])) {
-      $_SESSION['messages'][$type] = [];
-    }
-
-    // Convert strings which are safe to the simplest Markup objects.
-    if (!($message instanceof Markup) && $message instanceof MarkupInterface) {
-      $message = Markup::create((string) $message);
-    }
-
-    // Do not use strict type checking so that equivalent string and
-    // MarkupInterface objects are detected.
-    if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
-      $_SESSION['messages'][$type][] = $message;
-    }
-
-    // Mark this page as being uncacheable.
-    \Drupal::service('page_cache_kill_switch')->trigger();
-  }
-
-  // Messages not set when DB connection fails.
-  return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
+  /* @var \Drupal\Core\Messenger\MessengerInterface $messenger */
+  $messenger = \Drupal::service('messenger');
+  $messenger->addMessage($message, $type, $repeat);
+  return $messenger->all();
 }
 
 /**
@@ -507,12 +491,20 @@ function drupal_set_message($message = NULL, $type = 'status', $repeat = FALSE)
  *
  * @see drupal_set_message()
  * @see status-messages.html.twig
+ *
+ * @deprecated in Drupal 8.5.0, will be removed before Drupal 9.0.0.
+ *   Use \Drupal::service('messenger')->getMessages() or
+ *   \Drupal::service('messenger')->getMessagesByType() instead.
+ *
+ * @see https://www.drupal.org/node/2774931
  */
 function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
-  if ($messages = drupal_set_message()) {
+  /** @var \Drupal\Core\Messenger\MessengerInterface $messenger */
+  $messenger = \Drupal::hasService('messenger') ? \Drupal::service('messenger') : NULL;
+  if ($messenger && ($messages = $messenger->all())) {
     if ($type) {
       if ($clear_queue) {
-        unset($_SESSION['messages'][$type]);
+        $messenger->deleteByType($type);
       }
       if (isset($messages[$type])) {
         return [$type => $messages[$type]];
@@ -520,7 +512,7 @@ function drupal_get_messages($type = NULL, $clear_queue = TRUE) {
     }
     else {
       if ($clear_queue) {
-        unset($_SESSION['messages']);
+        $messenger->deleteAll();
       }
       return $messages;
     }
diff --git a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
index ba82988a14..c2688edeae 100644
--- a/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
+++ b/core/lib/Drupal/Core/EventSubscriber/MaintenanceModeSubscriber.php
@@ -5,6 +5,7 @@
 use Drupal\Component\Utility\SafeMarkup;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Render\BareHtmlPageRendererInterface;
+use Drupal\Core\Messenger\MessengerInterface;
 use Drupal\Core\Routing\RouteMatch;
 use Drupal\Core\Routing\UrlGeneratorInterface;
 use Drupal\Core\Session\AccountInterface;
@@ -59,6 +60,13 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
   protected $bareHtmlPageRenderer;
 
   /**
+   * The messenger.
+   *
+   * @var \Drupal\Core\Messenger\MessengerInterface
+   */
+  protected $messenger;
+
+  /**
    * Constructs a new MaintenanceModeSubscriber.
    *
    * @param \Drupal\Core\Site\MaintenanceModeInterface $maintenance_mode
@@ -73,14 +81,17 @@ class MaintenanceModeSubscriber implements EventSubscriberInterface {
    *   The current user.
    * @param \Drupal\Core\Render\BareHtmlPageRendererInterface $bare_html_page_renderer
    *   The bare HTML page renderer.
+   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
+   *   The messenger.
    */
-  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer) {
+  public function __construct(MaintenanceModeInterface $maintenance_mode, ConfigFactoryInterface $config_factory, TranslationInterface $translation, UrlGeneratorInterface $url_generator, AccountInterface $account, BareHtmlPageRendererInterface $bare_html_page_renderer, MessengerInterface $messenger) {
     $this->maintenanceMode = $maintenance_mode;
     $this->config = $config_factory;
     $this->stringTranslation = $translation;
     $this->urlGenerator = $url_generator;
     $this->account = $account;
     $this->bareHtmlPageRenderer = $bare_html_page_renderer;
+    $this->messenger = $messenger;
   }
 
   /**
@@ -118,10 +129,10 @@ public function onKernelRequestMaintenance(GetResponseEvent $event) {
         // settings page.
         if ($route_match->getRouteName() != 'system.site_maintenance_mode') {
           if ($this->account->hasPermission('administer site configuration')) {
-            $this->drupalSetMessage($this->t('Operating in maintenance mode. <a href=":url">Go online.</a>', [':url' => $this->urlGenerator->generate('system.site_maintenance_mode')]), 'status', FALSE);
+            $this->messenger->addMessage($this->t('Operating in maintenance mode. <a href=":url">Go online.</a>', [':url' => $this->urlGenerator->generate('system.site_maintenance_mode')]), 'status', FALSE);
           }
           else {
-            $this->drupalSetMessage($this->t('Operating in maintenance mode.'), 'status', FALSE);
+            $this->messenger->addMessage($this->t('Operating in maintenance mode.'), 'status', FALSE);
           }
         }
       }
@@ -141,13 +152,6 @@ protected function getSiteMaintenanceMessage() {
   }
 
   /**
-   * Wraps the drupal_set_message function.
-   */
-  protected function drupalSetMessage($message = NULL, $type = 'status', $repeat = FALSE) {
-    return drupal_set_message($message, $type, $repeat);
-  }
-
-  /**
    * {@inheritdoc}
    */
   public static function getSubscribedEvents() {
diff --git a/core/lib/Drupal/Core/Messenger/LegacyMessenger.php b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php
new file mode 100644
index 0000000000..93821313b3
--- /dev/null
+++ b/core/lib/Drupal/Core/Messenger/LegacyMessenger.php
@@ -0,0 +1,213 @@
+<?php
+
+namespace Drupal\Core\Messenger;
+
+use Drupal\Component\Render\MarkupInterface;
+use Drupal\Core\PageCache\ResponsePolicy\KillSwitch;
+use Drupal\Core\Render\Markup;
+
+/**
+ * A legacy implementation of the messenger interface.
+ *
+ * @internal
+ * @deprecated Deprecated as of Drupal 8.5.x
+ */
+class LegacyMessenger implements MessengerInterface {
+
+  /**
+   * The page cache kill switch.
+   *
+   * @var \Drupal\Core\PageCache\ResponsePolicy\KillSwitch
+   */
+  protected $killSwitch;
+
+  /**
+   * LegacyMessenger constructor.
+   *
+   * @param \Drupal\Core\PageCache\ResponsePolicy\KillSwitch $killSwitch
+   *   (optional) The page cache kill switch.
+   */
+  public function __construct(KillSwitch $killSwitch) {
+    $this->killSwitch = $killSwitch;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE) {
+    $this->setMessage($message, $type, $repeat);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addStatus($message, $repeat = FALSE) {
+    return $this->addMessage($message, static::TYPE_STATUS);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addError($message, $repeat = FALSE) {
+    return $this->addMessage($message, static::TYPE_ERROR);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addWarning($message, $repeat = FALSE) {
+    return $this->addMessage($message, static::TYPE_WARNING);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function all() {
+    return $this->getMessages(NULL, FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function messagesByType($type) {
+    return $this->getMessages($type, FALSE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteAll() {
+    return $this->getMessages(NULL, TRUE);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteByType($type) {
+    return $this->getMessages($type, TRUE);
+  }
+
+  /**
+   * Sets a message to display to the user.
+   *
+   * Messages are stored in a session variable and displayed in the page template
+   * via the $messages theme variable.
+   *
+   * Example usage:
+   * @code
+   * drupal_set_message(t('An error occurred and processing did not complete.'), 'error');
+   * @endcode
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $message
+   *   (optional) The translated message to be displayed to the user. For
+   *   consistency with other messages, it should begin with a capital letter and
+   *   end with a period.
+   * @param string $type
+   *   (optional) The message's type. Defaults to 'status'. These values are
+   *   supported:
+   *   - 'status'
+   *   - 'warning'
+   *   - 'error'
+   * @param bool $repeat
+   *   (optional) If this is FALSE and the message is already set, then the
+   *   message won't be repeated. Defaults to FALSE.
+   *
+   * @return array|null
+   *   A multidimensional array with keys corresponding to the set message types.
+   *   The indexed array values of each contain the set messages for that type,
+   *   and each message is an associative array with the following format:
+   *   - safe: Boolean indicating whether the message string has been marked as
+   *     safe. Non-safe strings will be escaped automatically.
+   *   - message: The message string.
+   *   So, the following is an example of the full return array structure:
+   *   @code
+   *     array(
+   *       'status' => array(
+   *         array(
+   *           'safe' => TRUE,
+   *           'message' => 'A <em>safe</em> markup string.',
+   *         ),
+   *         array(
+   *           'safe' => FALSE,
+   *           'message' => "$arbitrary_user_input to escape.",
+   *         ),
+   *       ),
+   *     );
+   *   @endcode
+   *   If there are no messages set, the function returns NULL.
+   *
+   * @internal
+   */
+  private function setMessage($message = NULL, $type = 'status', $repeat = FALSE) {
+    if (isset($message)) {
+      if (!isset($_SESSION['messages'][$type])) {
+        $_SESSION['messages'][$type] = [];
+      }
+
+      // Convert strings which are safe to the simplest Markup objects.
+      if (!($message instanceof Markup) && $message instanceof MarkupInterface) {
+        $message = Markup::create((string) $message);
+      }
+
+      // Do not use strict type checking so that equivalent string and
+      // MarkupInterface objects are detected.
+      if ($repeat || !in_array($message, $_SESSION['messages'][$type])) {
+        $_SESSION['messages'][$type][] = $message;
+      }
+
+      // Mark this page as being uncacheable.
+      $this->killSwitch->trigger();
+    }
+
+    // Messages not set when DB connection fails.
+    return isset($_SESSION['messages']) ? $_SESSION['messages'] : NULL;
+  }
+
+  /**
+   * Returns all messages that have been set with drupal_set_message().
+   *
+   * @param string $type
+   *   (optional) Limit the messages returned by type. Defaults to NULL, meaning
+   *   all types. These values are supported:
+   *   - NULL
+   *   - 'status'
+   *   - 'warning'
+   *   - 'error'
+   * @param bool $clear_queue
+   *   (optional) If this is TRUE, the queue will be cleared of messages of the
+   *   type specified in the $type parameter. Otherwise the queue will be left
+   *   intact. Defaults to TRUE.
+   *
+   * @return array
+   *   An associative, nested array of messages grouped by message type, with
+   *   the top-level keys as the message type. The messages returned are
+   *   limited to the type specified in the $type parameter, if any. If there
+   *   are no messages of the specified type, an empty array is returned. See
+   *   drupal_set_message() for the array structure of individual messages.
+   *
+   * @see drupal_set_message()
+   * @see status-messages.html.twig
+   *
+   * @internal
+   */
+  private function getMessages($type = NULL, $clear_queue = TRUE) {
+    if ($messages = $this->setMessage()) {
+      if ($type) {
+        if ($clear_queue) {
+          unset($_SESSION['messages'][$type]);
+        }
+        if (isset($messages[$type])) {
+          return [$type => $messages[$type]];
+        }
+      }
+      else {
+        if ($clear_queue) {
+          unset($_SESSION['messages']);
+        }
+        return $messages;
+      }
+    }
+    return [];
+  }
+
+}
diff --git a/core/lib/Drupal/Core/Messenger/MessengerInterface.php b/core/lib/Drupal/Core/Messenger/MessengerInterface.php
new file mode 100644
index 0000000000..941a3eebcc
--- /dev/null
+++ b/core/lib/Drupal/Core/Messenger/MessengerInterface.php
@@ -0,0 +1,127 @@
+<?php
+
+namespace Drupal\Core\Messenger;
+
+/**
+ * Stores runtime messages sent out to individual users on the page.
+ *
+ * An example for these messages is for example: "Content X got saved".
+ */
+interface MessengerInterface {
+
+  /**
+   * A status message.
+   */
+  const TYPE_STATUS = 'status';
+
+  /**
+   * A warning.
+   */
+  const TYPE_WARNING = 'warning';
+
+  /**
+   * An error.
+   */
+  const TYPE_ERROR = 'error';
+
+  /**
+   * Adds a new message to the queue.
+   *
+   * The messages will be displayed in the order they got added later.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $message
+   *   (optional) The translated message to be displayed to the user. For
+   *   consistency with other messages, it should begin with a capital letter
+   *   and end with a period.
+   * @param string $type
+   *   (optional) The message's type. Either self::TYPE_STATUS,
+   *   self::TYPE_WARNING, or self::TYPE_ERROR.
+   * @param bool $repeat
+   *   (optional) If this is FALSE and the message is already set, then the
+   *   message won't be repeated. Defaults to FALSE.
+   *
+   * @return $this
+   */
+  public function addMessage($message, $type = self::TYPE_STATUS, $repeat = FALSE);
+
+  /**
+   * Adds a new status message to the queue.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $message
+   *   (optional) The translated message to be displayed to the user. For
+   *   consistency with other messages, it should begin with a capital letter
+   *   and end with a period.
+   * @param bool $repeat
+   *   (optional) If this is FALSE and the message is already set, then the
+   *   message won't be repeated. Defaults to FALSE.
+   *
+   * @return $this
+   */
+  public function addStatus($message, $repeat = FALSE);
+
+  /**
+   * Adds a new error message to the queue.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $message
+   *   (optional) The translated message to be displayed to the user. For
+   *   consistency with other messages, it should begin with a capital letter
+   *   and end with a period.
+   * @param bool $repeat
+   *   (optional) If this is FALSE and the message is already set, then the
+   *   message won't be repeated. Defaults to FALSE.
+   *
+   * @return $this
+   */
+  public function addError($message, $repeat = FALSE);
+
+  /**
+   * Adds a new warning message to the queue.
+   *
+   * @param string|\Drupal\Component\Render\MarkupInterface $message
+   *   (optional) The translated message to be displayed to the user. For
+   *   consistency with other messages, it should begin with a capital letter
+   *   and end with a period.
+   * @param bool $repeat
+   *   (optional) If this is FALSE and the message is already set, then the
+   *   message won't be repeated. Defaults to FALSE.
+   *
+   * @return $this
+   */
+  public function addWarning($message, $repeat = FALSE);
+
+  /**
+   * Gets all messages.
+   *
+   * @return string[][]|\Drupal\Component\Render\MarkupInterface[][]
+   *   Keys are message types and values are indexed arrays of messages. Message
+   *   types are either self::TYPE_STATUS, self::TYPE_WARNING, or
+   *   self::TYPE_ERROR.
+   */
+  public function all();
+
+  /**
+   * Gets all messages of a certain type.
+   *
+   * @param string $type
+   *   The messages' type. Either self::TYPE_STATUS, self::TYPE_WARNING,
+   *   or self::TYPE_ERROR.
+   *
+   * @return string[]|\Drupal\Component\Render\MarkupInterface[]
+   */
+  public function messagesByType($type);
+
+  /**
+   * Deletes all messages.
+   */
+  public function deleteAll();
+
+  /**
+   * Deletes all messages of a certain type.
+   *
+   * @param string $type
+   *   The messages' type. Either self::TYPE_STATUS, self::TYPE_WARNING, or
+   *   self::TYPE_ERROR.
+   */
+  public function deleteByType($type);
+
+}
