diff --git a/uc_ajax_admin/uc_ajax_admin.module b/uc_ajax_admin/uc_ajax_admin.module
index abf792a..398870a 100644
--- a/uc_ajax_admin/uc_ajax_admin.module
+++ b/uc_ajax_admin/uc_ajax_admin.module
@@ -47,10 +47,10 @@ function uc_ajax_admin_form($form, &$form_state, $target_form = 'checkout') {
   switch ($target_form) {
     case 'checkout':
       $triggers = _uc_ajax_admin_checkout_trigger_options(_uc_ajax_admin_build_checkout_form());
-      $panes = _uc_checkout_pane_list();
+      $panes = \Drupal::service('plugin.manager.uc_cart.checkout_pane')->getDefinitions();
       $wrappers = array();
       foreach ($panes as $id => $pane) {
-        $wrappers["$id-pane"] = _uc_checkout_pane_data($id, 'title');
+        $wrappers["$id-pane"] = $pane['title'];
       }
       break;
 
diff --git a/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginBase.php b/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginBase.php
new file mode 100644
index 0000000..aee44bf
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginBase.php
@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\CheckoutPanePluginBase.
+ */
+
+namespace Drupal\uc_cart;
+
+use Drupal\Core\Plugin\PluginBase;
+
+/**
+ * Defines a base checkout pane plugin implementation.
+ */
+abstract class CheckoutPanePluginBase extends PluginBase implements CheckoutPanePluginInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getTitle() {
+    return $this->pluginDefinition['title'];
+  }
+
+}
diff --git a/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginInterface.php b/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginInterface.php
new file mode 100644
index 0000000..93d603c
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/CheckoutPanePluginInterface.php
@@ -0,0 +1,68 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\CheckoutPanePluginInterface.
+ */
+
+namespace Drupal\uc_cart;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\uc_order\UcOrderInterface;
+
+/**
+ * Defines an interface for checkout pane plugins.
+ */
+interface CheckoutPanePluginInterface extends PluginInspectionInterface {
+
+  /**
+   * Prepares a pane for display.
+   *
+   * @param \Drupal\uc_order\UcOrderInterface $order
+   *   The order that is being processed.
+   * @param array $form
+   *   The checkout form array.
+   * @param array $form_state
+   *   The checkout form state array.
+   */
+  public function prepare(UcOrderInterface $order, array $form, array &$form_state);
+
+  /**
+   * Returns the contents of a checkout pane.
+   *
+   * @param \Drupal\uc_order\UcOrderInterface $order
+   *   The order that is being processed.
+   * @param array $form
+   *   The checkout form array.
+   * @param array $form_state
+   *   The checkout form state array.
+   *
+   * @return array
+   *   A form array.
+   */
+  public function view(UcOrderInterface $order, array $form, array &$form_state);
+
+  /**
+   * Processes a checkout pane.
+   *
+   * @param \Drupal\uc_order\UcOrderInterface $order
+   *   The order that is being processed.
+   * @param array $form
+   *   The checkout form array.
+   * @param array $form_state
+   *   The checkout form state array.
+   *
+   * @return bool
+   *   TRUE if the pane is valid, FALSE otherwise..
+   */
+  public function process(UcOrderInterface $order, array $form, array &$form_state);
+
+  /**
+   * Returns the title of the checkout pane.
+   *
+   * @return string
+   *   The pane title.
+   */
+  public function getTitle();
+
+}
diff --git a/uc_cart/lib/Drupal/uc_cart/Controller/CheckoutController.php b/uc_cart/lib/Drupal/uc_cart/Controller/CheckoutController.php
index c3f1d36..a684c03 100644
--- a/uc_cart/lib/Drupal/uc_cart/Controller/CheckoutController.php
+++ b/uc_cart/lib/Drupal/uc_cart/Controller/CheckoutController.php
@@ -140,23 +140,18 @@ class CheckoutController extends ControllerBase {
       return $this->redirect('uc_cart.cart');
     }
 
-    $panes = _uc_checkout_pane_list();
+    $filter = array('enabled' => FALSE);
 
-    $cart_config = \Drupal::config('uc_cart.settings');
     // If the cart isn't shippable, bypass panes with shippable == TRUE.
-    if (!$order->isShippable() && $cart_config->get('delivery_not_shippable')) {
-      $panes = uc_cart_filter_checkout_panes($panes, array('shippable' => TRUE));
+    if (!$order->isShippable() && \Drupal::config('uc_cart.settings')->get('delivery_not_shippable')) {
+      $filter['shippable'] = TRUE;
     }
 
+    $panes = $this->container()->get('plugin.manager.uc_cart.checkout_pane')->getDefinitions($filter);
     foreach ($panes as $pane) {
-      if ($pane['enabled']) {
-        $func = $pane['callback'];
-        if (function_exists($func)) {
-          $return = $func('review', $order, NULL);
-          if (!is_null($return)) {
-            $data[$pane['title']] = $return;
-          }
-        }
+      $return = $pane['callback']('review', $order, NULL);
+      if (!is_null($return)) {
+        $data[$pane['title']] = $return;
       }
     }
 
diff --git a/uc_cart/lib/Drupal/uc_cart/Form/CheckoutForm.php b/uc_cart/lib/Drupal/uc_cart/Form/CheckoutForm.php
index 6fea501..f02c190 100644
--- a/uc_cart/lib/Drupal/uc_cart/Form/CheckoutForm.php
+++ b/uc_cart/lib/Drupal/uc_cart/Form/CheckoutForm.php
@@ -35,19 +35,21 @@ class CheckoutForm extends FormBase {
     $form['#attributes']['class'][] = 'uc-cart-checkout-form';
     $form['#attached']['css'][] = drupal_get_path('module', 'uc_cart') . '/css/uc_cart.css';
     $form['panes'] = array('#tree' => TRUE);
-    $panes = _uc_checkout_pane_list();
+
+    $filter = array('enabled' => FALSE);
 
     // If the order isn't shippable, remove panes with shippable == TRUE.
-    $cart_config = \Drupal::config('uc_cart.settings');
-    if (!$order->isShippable() && $cart_config->get('delivery_not_shippable')) {
-      $panes = uc_cart_filter_checkout_panes($panes, array('shippable' => TRUE));
+    if (!$order->isShippable() && \Drupal::config('uc_cart.settings')->get('delivery_not_shippable')) {
+      $filter['shippable'] = TRUE;
     }
 
+    $panes = $this->container()->get('plugin.manager.uc_cart.checkout_pane')->createInstances($filter);
+
     // Invoke the 'prepare' op of enabled panes, but only if their 'process' ops
     // have not been invoked on this request (i.e. when rebuilding after AJAX).
     foreach ($panes as $id => $pane) {
-      if ($pane['enabled'] && empty($form_state['storage']['panes'][$id]['prepared']) && isset($pane['callback']) && function_exists($pane['callback'])) {
-        $pane['callback']('prepare', $order, $form, $form_state);
+      if (empty($form_state['storage']['panes'][$id]['prepared'])) {
+        $pane->prepare($order, $form, $form_state);
         $form_state['storage']['panes'][$id]['prepared'] = TRUE;
         $processed = FALSE; // Make sure we save the updated order.
       }
@@ -62,26 +64,12 @@ class CheckoutForm extends FormBase {
     }
 
     foreach ($panes as $id => $pane) {
-      if ($pane['enabled']) {
-        $return = $pane['callback']('view', $order, $form, $form_state);
-
-        // Add the pane if any display data is returned from the callback.
-        if (is_array($return) && (!empty($return['description']) || !empty($return['contents']))) {
-          // Create the fieldset for the pane.
-          $form['panes'][$id] = array(
-            '#type' => 'details',
-            '#title' => check_plain($pane['title']),
-            '#description' => !empty($return['description']) ? $return['description'] : '',
-            '#id' => $id . '-pane',
-            '#theme' => isset($return['theme']) ? $return['theme'] : NULL,
-          );
-
-          // Add the contents of the fieldset if any were returned.
-          if (!empty($return['contents'])) {
-            $form['panes'][$id] = array_merge($form['panes'][$id], $return['contents']);
-          }
-        }
-      }
+      $form['panes'][$id] = $pane->view($order, $form, $form_state);
+      $form['panes'][$id] += array(
+        '#type' => 'details',
+        '#title' => check_plain($pane->getTitle()),
+        '#id' => $id . '-pane',
+      );
     }
 
     $form['actions'] = array('#type' => 'actions');
@@ -117,13 +105,10 @@ class CheckoutForm extends FormBase {
 
     // Validate/process the cart panes.  A FALSE value results in failed checkout.
     $form_state['checkout_valid'] = TRUE;
-    foreach (element_children($form_state['values']['panes']) as $pane_id) {
-      $func = _uc_checkout_pane_data($pane_id, 'callback');
-      if (is_string($func) && function_exists($func)) {
-        $isvalid = $func('process', $order, $form, $form_state);
-        if ($isvalid === FALSE) {
-          $form_state['checkout_valid'] = FALSE;
-        }
+    foreach (element_children($form_state['values']['panes']) as $id) {
+      $pane = $this->container()->get('plugin.manager.uc_cart.checkout_pane')->createInstance($id);
+      if ($pane->process($order, $form, $form_state) === FALSE) {
+        $form_state['checkout_valid'] = FALSE;
       }
     }
 
diff --git a/uc_cart/lib/Drupal/uc_cart/Form/CheckoutSettingsForm.php b/uc_cart/lib/Drupal/uc_cart/Form/CheckoutSettingsForm.php
index 35d4a46..afc9612 100644
--- a/uc_cart/lib/Drupal/uc_cart/Form/CheckoutSettingsForm.php
+++ b/uc_cart/lib/Drupal/uc_cart/Form/CheckoutSettingsForm.php
@@ -118,7 +118,7 @@ class CheckoutSettingsForm extends ConfigFormBase {
       '#states' => $anon_state,
     );
 
-    $panes = _uc_checkout_pane_list();
+    $panes = $this->container()->get('plugin.manager.uc_cart.checkout_pane')->getDefinitions();
     $form['checkout']['panes'] = array(
       '#type' => 'table',
       '#header' => array(t('Pane'), t('List position')),
diff --git a/uc_cart/lib/Drupal/uc_cart/Plugin/CheckoutPaneManager.php b/uc_cart/lib/Drupal/uc_cart/Plugin/CheckoutPaneManager.php
new file mode 100644
index 0000000..af1212b
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/Plugin/CheckoutPaneManager.php
@@ -0,0 +1,110 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\Plugin\CheckoutPaneManager.
+ */
+
+namespace Drupal\uc_cart\Plugin;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Language\LanguageManager;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Plugin\Discovery\AnnotatedClassDiscovery;
+use Drupal\Core\Plugin\Factory\ContainerFactory;
+use Drupal\uc_store\Plugin\Discovery\InfoHookDecorator;
+
+/**
+ * Manages discovery and instantiation of checkout panes.
+ */
+class CheckoutPaneManager extends DefaultPluginManager {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaults = array(
+    'enabled' => TRUE,
+    'weight' => 0,
+    'review' => TRUE,
+    'process' => TRUE,
+    'collapsible' => TRUE,
+  );
+
+  /**
+   * Constructs a CheckoutPaneManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations,
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Language\LanguageManager $language_manager
+   *   The language manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, LanguageManager $language_manager, ModuleHandlerInterface $module_handler) {
+    $this->discovery = new AnnotatedClassDiscovery('Plugin/Ubercart/CheckoutPane', $namespaces, 'Drupal\Component\Annotation\Plugin');
+    $this->discovery = new InfoHookDecorator($this->discovery, 'uc_checkout_pane', 'Drupal\uc_cart\Plugin\Ubercart\CheckoutPane\LegacyCheckoutPane');
+    $this->factory = new ContainerFactory($this);
+
+    $this->alterInfo($module_handler, 'uc_checkout_pane');
+    $this->setCacheBackend($cache_backend, $language_manager, 'uc_checkout_panes');
+  }
+
+  /**
+   * Gets instances of checkout pane plugins, optionally filtered.
+   *
+   * @param array $filter
+   *   An array of definition keys to filter by.
+   *
+   * @return array
+   *   An array of checkout pane plugin instances.
+   */
+  public function createInstances($filter = array()) {
+    $definitions = $this->getDefinitions($filter);
+
+    $instances = array();
+    foreach (array_keys($definitions) as $id) {
+      $instances[$id] = $this->createInstance($id);
+    }
+    return $instances;
+  }
+
+  /**
+   * Gets the definition of checkout pane plugins, optionally filtered.
+   *
+   * @param array $filter
+   *   An array of definition keys to filter by.
+   *
+   * @return array
+   *   An array of checkout pane plugin definitions.
+   */
+  public function getDefinitions($filter = array()) {
+    $panes = parent::getDefinitions();
+
+    foreach ($panes as $id => $pane) {
+      foreach ($filter as $key => $value) {
+        if (isset($panes[$id][$key]) && $panes[$id][$key] == $value) {
+          unset($panes[$id]);
+        }
+      }
+    }
+
+    uasort($panes, 'uc_weight_sort');
+
+    return $panes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function processDefinition(&$definition, $plugin_id) {
+    parent::processDefinition($definition, $plugin_id);
+
+    $definition['enabled'] = variable_get('uc_pane_' . $plugin_id . '_enabled', $definition['enabled']);
+    $definition['weight'] = variable_get('uc_pane_' . $plugin_id . '_weight', $definition['weight']);
+  }
+
+}
diff --git a/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/LegacyCheckoutPane.php b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/LegacyCheckoutPane.php
new file mode 100644
index 0000000..b034c3e
--- /dev/null
+++ b/uc_cart/lib/Drupal/uc_cart/Plugin/Ubercart/CheckoutPane/LegacyCheckoutPane.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\uc_cart\Plugin\Ubercart\CheckoutPane\LegacyCheckoutPane.
+ */
+
+namespace Drupal\uc_cart\Plugin\Ubercart\CheckoutPane;
+
+use Drupal\uc_cart\CheckoutPanePluginBase;
+use Drupal\uc_order\UcOrderInterface;
+
+/**
+ * Defines a plugin implementation for legacy checkout panes.
+ */
+class LegacyCheckoutPane extends CheckoutPanePluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function prepare(UcOrderInterface $order, array $form, array &$form_state) {
+    $this->pluginDefinition['callback']('prepare', $order, $form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function view(UcOrderInterface $order, array $form, array &$form_state) {
+    $pane = $this->pluginDefinition['callback']('view', $order, $form, $form_state);
+
+    $build = $pane['contents'];
+    if (isset($pane['description'])) {
+      $build['#description'] = $pane['description'];
+    }
+
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process(UcOrderInterface $order, array $form, array &$form_state) {
+    $this->pluginDefinition['callback']('process', $order, $form, $form_state);
+  }
+
+}
diff --git a/uc_cart/uc_cart.module b/uc_cart/uc_cart.module
index 9f2c5b6..3e23011 100644
--- a/uc_cart/uc_cart.module
+++ b/uc_cart/uc_cart.module
@@ -685,22 +685,3 @@ function uc_cart_is_shippable($cart_id = NULL) {
 
   return FALSE;
 }
-
-/**
- * Removes panes from the list that match the given conditions.
- *
- * @return
- *   A checkout pane array with panes filtered out that have key values
- *   matching the combinations in the $remove array.
- */
-function uc_cart_filter_checkout_panes($panes, $remove) {
-  foreach ($panes as $id => $pane) {
-    foreach ($remove as $key => $value) {
-      if (isset($panes[$id][$key]) && $panes[$id][$key] == $value) {
-        unset($panes[$id]);
-      }
-    }
-  }
-
-  return $panes;
-}
diff --git a/uc_cart/uc_cart.services.yml b/uc_cart/uc_cart.services.yml
index db486e3..bb2a36f 100644
--- a/uc_cart/uc_cart.services.yml
+++ b/uc_cart/uc_cart.services.yml
@@ -1,4 +1,7 @@
 services:
+  plugin.manager.uc_cart.checkout_pane:
+    class: Drupal\uc_cart\Plugin\CheckoutPaneManager
+    parent: default_plugin_manager
   uc_cart.breadcrumb:
     class: Drupal\uc_cart\CartBreadcrumbBuilder
     tags:
diff --git a/uc_cart/uc_cart_checkout_pane.inc b/uc_cart/uc_cart_checkout_pane.inc
index 501c289..c74ad05 100644
--- a/uc_cart/uc_cart_checkout_pane.inc
+++ b/uc_cart/uc_cart_checkout_pane.inc
@@ -368,55 +368,6 @@ function uc_checkout_pane_comments($op, $order, $form = NULL, &$form_state = NUL
 }
 
 /**
- * Builds a list of checkout panes defined in the enabled modules.
- */
-function _uc_checkout_pane_list($action = NULL) {
-  static $panes = array();
-
-  if (count($panes) > 0 && $action !== 'rebuild') {
-    return $panes;
-  }
-
-  $cart_config = \Drupal::config('uc_cart.settings');
-  foreach (\Drupal::moduleHandler()->invokeAll('uc_checkout_pane') as $id => $pane) {
-    // Preserve backward compatibility for panes with no key specified.
-    if (is_numeric($id)) {
-      $id = $pane['id'];
-    }
-
-    // Set defaults.
-    $pane += array(
-      'id' => $id,
-      'enabled' => TRUE,
-      'weight' => 0,
-      'review' => TRUE,
-      'process' => TRUE,
-      'collapsible' => TRUE,
-    );
-
-    $pane['enabled'] = variable_get('uc_pane_' . $id . '_enabled', $pane['enabled']);
-    $pane['weight'] = variable_get('uc_pane_' . $id . '_weight', $pane['weight']);
-
-    $panes[$id] = $pane;
-  }
-
-  // Allow other modules to alter the defaults.
-  \Drupal::moduleHandler()->alter('uc_checkout_pane', $panes);
-
-  uasort($panes, 'uc_weight_sort');
-
-  return $panes;
-}
-
-/**
- * Returns data from a checkout pane by pane ID and the array key.
- */
-function _uc_checkout_pane_data($pane_id, $key) {
-  $panes = _uc_checkout_pane_list();
-  return $panes[$pane_id][$key];
-}
-
-/**
  * Formats the cart contents table on the checkout page.
  *
  * @param $variables
