diff --git a/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..f1b031d4a265b4a444c551180c20a1cb2cd5e0d9
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyEntityHooks.php
@@ -0,0 +1,184 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\taxonomy\Entity\Term;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
+use Drupal\node\NodeInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\Core\Url;
+
+/**
+ * Entity hook implementations for taxonomy.
+ */
+class TaxonomyEntityHooks {
+
+  use StringTranslationTrait;
+
+  public function __construct(
+    protected ConfigFactoryInterface $configFactory,
+    protected Connection $database,
+    protected EntityTypeManagerInterface $entityTypeManager,
+  ) {}
+
+  /**
+   * Returns the maintain_index_table configuration value.
+   */
+  protected function shouldMaintainIndexTable(): bool {
+    return (bool) $this->configFactory->get('taxonomy.settings')->get('maintain_index_table');
+  }
+
+  /**
+   * Builds and inserts taxonomy index entries for a given node.
+   *
+   * The index lists all terms that are related to a given node entity, and is
+   * therefore maintained at the entity level.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The node entity.
+   */
+  protected function buildNodeIndex(NodeInterface $node): void {
+    // We maintain a denormalized table of term/node relationships, containing
+    // only data for current, published nodes.
+    if (!$this->shouldMaintainIndexTable() || !($this->entityTypeManager->getStorage('node') instanceof SqlContentEntityStorage)) {
+      return;
+    }
+
+    $status = $node->isPublished();
+    $sticky = (int) $node->isSticky();
+    // We only maintain the taxonomy index for published nodes.
+    if ($status && $node->isDefaultRevision()) {
+      // Collect a unique list of all the term IDs from all node fields.
+      $tid_all = [];
+      $entity_reference_class = 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem';
+      foreach ($node->getFieldDefinitions() as $field) {
+        $field_name = $field->getName();
+        $class = $field->getItemDefinition()->getClass();
+        $is_entity_reference_class = ($class === $entity_reference_class) || is_subclass_of($class, $entity_reference_class);
+        if ($is_entity_reference_class && $field->getSetting('target_type') == 'taxonomy_term') {
+          foreach ($node->getTranslationLanguages() as $language) {
+            foreach ($node->getTranslation($language->getId())->$field_name as $item) {
+              if (!$item->isEmpty()) {
+                $tid_all[$item->target_id] = $item->target_id;
+              }
+            }
+          }
+        }
+      }
+      // Insert index entries for all the node's terms.
+      if (!empty($tid_all)) {
+        foreach ($tid_all as $tid) {
+          $this->database->merge('taxonomy_index')
+            ->keys(['nid' => $node->id(), 'tid' => $tid, 'status' => $node->isPublished()])
+            ->fields(['sticky' => $sticky, 'created' => $node->getCreatedTime()])
+            ->execute();
+        }
+      }
+    }
+  }
+
+  /**
+   * Deletes taxonomy index entries for a given node.
+   *
+   * @param \Drupal\node\NodeInterface $node
+   *   The node entity.
+   */
+  protected function deleteNodeIndex(NodeInterface $node): void {
+    if ($this->shouldMaintainIndexTable()) {
+      $this->database->delete('taxonomy_index')->condition('nid', $node->id())->execute();
+    }
+  }
+
+  /**
+   * Implements hook_entity_operation().
+   */
+  #[Hook('entity_operation')]
+  public function entityOperation(EntityInterface $term): array {
+    $operations = [];
+    if ($term instanceof Term && $term->access('create')) {
+      $operations['add-child'] = [
+        'title' => $this->t('Add child'),
+        'weight' => 10,
+        'url' => Url::fromRoute('entity.taxonomy_term.add_form', [
+          'taxonomy_vocabulary' => $term->bundle(),
+        ], [
+          'query' => [
+            'parent' => $term->id(),
+          ],
+        ]),
+      ];
+    }
+    return $operations;
+  }
+
+  /**
+   * @defgroup taxonomy_index Taxonomy indexing
+   * @{
+   * Functions to maintain taxonomy indexing.
+   *
+   * Taxonomy uses default field storage to store canonical relationships
+   * between terms and fieldable entities. However its most common use case
+   * requires listing all content associated with a term or group of terms
+   * sorted by creation date. To avoid slow queries due to joining across
+   * multiple node and field tables with various conditions and order by
+   * criteria, we maintain a denormalized table with all relationships between
+   * terms, published nodes and common sort criteria such as status, sticky and
+   * created. When using other field storage engines or alternative methods of
+   * denormalizing this data you should set the
+   * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes
+   * in SQL.
+   */
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert() for node entities.
+   */
+  #[Hook('node_insert')]
+  public function nodeInsert(EntityInterface $node): void {
+    // Add taxonomy index entries for the node.
+    $this->buildNodeIndex($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update() for node entities.
+   */
+  #[Hook('node_update')]
+  public function nodeUpdate(EntityInterface $node): void {
+    // If we're not dealing with the default revision of the node, do not make any
+    // change to the taxonomy index.
+    if (!$node->isDefaultRevision()) {
+      return;
+    }
+    $this->deleteNodeIndex($node);
+    $this->buildNodeIndex($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_predelete() for node entities.
+   */
+  #[Hook('node_predelete')]
+  public function nodePredelete(EntityInterface $node): void {
+    // Clean up the {taxonomy_index} table when nodes are deleted.
+    $this->deleteNodeIndex($node);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
+   */
+  #[Hook('taxonomy_term_delete')]
+  public function taxonomyTermDelete(Term $term): void {
+    if ($this->shouldMaintainIndexTable()) {
+      // Clean up the {taxonomy_index} table when terms are deleted.
+      $this->database->delete('taxonomy_index')->condition('tid', $term->id())->execute();
+    }
+  }
+
+  // phpcs:ignore Drupal.Commenting.InlineComment.DocBlock
+  /**
+   * @} End of "defgroup taxonomy_index".
+   */
+}
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php
similarity index 57%
rename from core/modules/taxonomy/src/Hook/TaxonomyHooks.php
rename to core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php
index 4cc0a6f6eb613ce32ebdeb0edf1dd40b14a25b49..2692e0d4e22d7584d2e3658e6585732286e49061 100644
--- a/core/modules/taxonomy/src/Hook/TaxonomyHooks.php
+++ b/core/modules/taxonomy/src/Hook/TaxonomyHelpHooks.php
@@ -2,28 +2,29 @@
 
 namespace Drupal\taxonomy\Hook;
 
-use Drupal\Core\StringTranslation\StringTranslationTrait;
-use Drupal\taxonomy\Entity\Term;
-use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Url;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
 
 /**
- * Hook implementations for taxonomy.
+ * Help hook implementation for the Taxonomy module.
  */
-class TaxonomyHooks {
+class TaxonomyHelpHooks {
 
   use StringTranslationTrait;
 
+  public function __construct(protected ModuleHandlerInterface $moduleHandler) {}
+
   /**
    * Implements hook_help().
    */
   #[Hook('help')]
-  public function help($route_name, RouteMatchInterface $route_match): ?string {
+  public function help(string $route_name, RouteMatchInterface $route_match): string {
     switch ($route_name) {
       case 'help.page.taxonomy':
-        $field_ui_url = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
+        $field_ui_url = $this->moduleHandler->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
         $output = '';
         $output .= '<h2>' . $this->t('About') . '</h2>';
         $output .= '<p>' . $this->t('The Taxonomy module allows users who have permission to create and edit content to categorize (tag) content of that type. Users who have the <em>Administer vocabularies and terms</em> <a href=":permissions" title="Taxonomy module permissions">permission</a> can add <em>vocabularies</em> that contain a set of related <em>terms</em>. The terms in a vocabulary can either be pre-set by an administrator or built gradually as content is added and edited. Terms may be organized hierarchically if desired.', [
@@ -69,114 +70,10 @@ public function help($route_name, RouteMatchInterface $route_match): ?string {
       case 'entity.taxonomy_vocabulary.collection':
         $output = '<p>' . $this->t('Taxonomy is for categorizing content. Terms are grouped into vocabularies. For example, a vocabulary called "Fruit" would contain the terms "Apple" and "Banana".') . '</p>';
         return $output;
-    }
-    return NULL;
-  }
-
-  /**
-   * Implements hook_theme().
-   */
-  #[Hook('theme')]
-  public function theme() : array {
-    return ['taxonomy_term' => ['render element' => 'elements']];
-  }
-
-  /**
-   * Implements hook_local_tasks_alter().
-   *
-   * @todo Evaluate removing as part of https://www.drupal.org/node/2358923.
-   */
-  #[Hook('local_tasks_alter')]
-  public function localTasksAlter(&$local_tasks): void {
-    $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview';
-    if (isset($local_tasks[$local_task_key])) {
-      // The config_translation module expects the base route to be
-      // entity.taxonomy_vocabulary.edit_form like it is for other configuration
-      // entities. Taxonomy uses the overview_form as the base route.
-      $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form';
-    }
-  }
-
-  /**
-   * Implements hook_entity_operation().
-   */
-  #[Hook('entity_operation')]
-  public function entityOperation(EntityInterface $term): array {
-    $operations = [];
-    if ($term instanceof Term && $term->access('create')) {
-      $operations['add-child'] = [
-        'title' => $this->t('Add child'),
-        'weight' => 10,
-        'url' => Url::fromRoute('entity.taxonomy_term.add_form', [
-          'taxonomy_vocabulary' => $term->bundle(),
-        ], [
-          'query' => [
-            'parent' => $term->id(),
-          ],
-        ]),
-      ];
-    }
-    return $operations;
-  }
 
-  /**
-   * @defgroup taxonomy_index Taxonomy indexing
-   * @{
-   * Functions to maintain taxonomy indexing.
-   *
-   * Taxonomy uses default field storage to store canonical relationships
-   * between terms and fieldable entities. However its most common use case
-   * requires listing all content associated with a term or group of terms
-   * sorted by creation date. To avoid slow queries due to joining across
-   * multiple node and field tables with various conditions and order by
-   * criteria, we maintain a denormalized table with all relationships between
-   * terms, published nodes and common sort criteria such as status, sticky and
-   * created. When using other field storage engines or alternative methods of
-   * denormalizing this data you should set the
-   * taxonomy.settings:maintain_index_table to '0' to avoid unnecessary writes
-   * in SQL.
-   */
-
-  /**
-   * Implements hook_ENTITY_TYPE_insert() for node entities.
-   */
-  #[Hook('node_insert')]
-  public function nodeInsert(EntityInterface $node): void {
-    // Add taxonomy index entries for the node.
-    taxonomy_build_node_index($node);
-  }
-
-  /**
-   * Implements hook_ENTITY_TYPE_update() for node entities.
-   */
-  #[Hook('node_update')]
-  public function nodeUpdate(EntityInterface $node): void {
-    // If we're not dealing with the default revision of the node, do not make
-    // any change to the taxonomy index.
-    if (!$node->isDefaultRevision()) {
-      return;
-    }
-    taxonomy_delete_node_index($node);
-    taxonomy_build_node_index($node);
-  }
-
-  /**
-   * Implements hook_ENTITY_TYPE_predelete() for node entities.
-   */
-  #[Hook('node_predelete')]
-  public function nodePredelete(EntityInterface $node): void {
-    // Clean up the {taxonomy_index} table when nodes are deleted.
-    taxonomy_delete_node_index($node);
-  }
-
-  /**
-   * Implements hook_ENTITY_TYPE_delete() for taxonomy_term entities.
-   */
-  #[Hook('taxonomy_term_delete')]
-  public function taxonomyTermDelete(Term $term): void {
-    if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
-      // Clean up the {taxonomy_index} table when terms are deleted.
-      \Drupal::database()->delete('taxonomy_index')->condition('tid', $term->id())->execute();
+      default:
+        $output = '';
+        return $output;
     }
   }
 
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..65e949cde1510871e9de225222492dd957deb4bc
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyMenuHooks.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Menu hook implementations for taxonomy.
+ */
+class TaxonomyMenuHooks {
+
+  /**
+   * Implements hook_local_tasks_alter().
+   *
+   * @todo Evaluate removing as part of https://www.drupal.org/node/2358923.
+   */
+  #[Hook('local_tasks_alter')]
+  public function localTasksAlter(array &$local_tasks): void {
+    $local_task_key = 'config_translation.local_tasks:entity.taxonomy_vocabulary.config_translation_overview';
+    if (isset($local_tasks[$local_task_key])) {
+      // The config_translation module expects the base route to be
+      // entity.taxonomy_vocabulary.edit_form like it is for other configuration
+      // entities. Taxonomy uses the overview_form as the base route.
+      $local_tasks[$local_task_key]['base_route'] = 'entity.taxonomy_vocabulary.overview_form';
+    }
+  }
+
+}
diff --git a/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php
new file mode 100644
index 0000000000000000000000000000000000000000..e62ba91ee7915f6a1c202071abeab9d5f6afb409
--- /dev/null
+++ b/core/modules/taxonomy/src/Hook/TaxonomyThemeHooks.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\taxonomy\Hook;
+
+use Drupal\Core\Hook\Attribute\Hook;
+
+/**
+ * Implements theme hook implementations for the Taxonomy module.
+ */
+class TaxonomyThemeHooks {
+
+  /**
+   * Implements hook_theme().
+   */
+  #[Hook('theme')]
+  public function taxonomyTheme(): array {
+    return [
+      'taxonomy_term' => [
+        'render element' => 'elements',
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_theme_suggestions_HOOK().
+   */
+  #[Hook('theme_suggestions_taxonomy_term')]
+  public function themeSuggestionsTaxonomyTerm(array $variables): array {
+    $suggestions = [];
+
+    /** @var \Drupal\taxonomy\TermInterface $term */
+    $term = $variables['elements']['#taxonomy_term'];
+
+    $suggestions[] = 'taxonomy_term__' . $term->bundle();
+    $suggestions[] = 'taxonomy_term__' . $term->id();
+
+    return $suggestions;
+  }
+
+}
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 04c00e36b6c25dc3f18830edcedb8ad44be65d11..ecc5fa0c4dbee04805ff6db9d7b4c3569e33ef1f 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -9,21 +9,6 @@
 use Drupal\Core\Render\Element;
 use Drupal\taxonomy\Entity\Term;
 
-/**
- * Implements hook_theme_suggestions_HOOK().
- */
-function taxonomy_theme_suggestions_taxonomy_term(array $variables): array {
-  $suggestions = [];
-
-  /** @var \Drupal\taxonomy\TermInterface $term */
-  $term = $variables['elements']['#taxonomy_term'];
-
-  $suggestions[] = 'taxonomy_term__' . $term->bundle();
-  $suggestions[] = 'taxonomy_term__' . $term->id();
-
-  return $suggestions;
-}
-
 /**
  * Prepares variables for taxonomy term templates.
  *
@@ -99,6 +84,7 @@ function taxonomy_term_is_page(Term $term) {
  *   The node entity.
  */
 function taxonomy_build_node_index($node): void {
+  @trigger_error('taxonomy_build_node_index() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3515362', E_USER_DEPRECATED);
   // We maintain a denormalized table of term/node relationships, containing
   // only data for current, published nodes.
   if (!\Drupal::config('taxonomy.settings')->get('maintain_index_table') || !(\Drupal::entityTypeManager()->getStorage('node') instanceof SqlContentEntityStorage)) {
@@ -146,11 +132,8 @@ function taxonomy_build_node_index($node): void {
  *   The node entity.
  */
 function taxonomy_delete_node_index(EntityInterface $node): void {
+  @trigger_error('taxonomy_delete_node_index() is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. See https://www.drupal.org/node/3515362', E_USER_DEPRECATED);
   if (\Drupal::config('taxonomy.settings')->get('maintain_index_table')) {
     \Drupal::database()->delete('taxonomy_index')->condition('nid', $node->id())->execute();
   }
 }
-
-/**
- * @} End of "defgroup taxonomy_index".
- */