diff --git a/composer.json b/composer.json
index 3810c88..270a6b5 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
     }
   },
   "require": {
-    "drupal/core": "^8.8 || ^9 || ^10 || ^11"
+    "drupal/core": "^10.1 || ^11 || ^12"
   },
   "extra": {
     "branch-alias": {
diff --git a/nodeaccess.info.yml b/nodeaccess.info.yml
index 7bb9ee6..a89ca55 100644
--- a/nodeaccess.info.yml
+++ b/nodeaccess.info.yml
@@ -3,7 +3,7 @@ description: Provides per node access control.
 package: Access control
 configure: nodeaccess.administration
 type: module
-core_version_requirement: ^8.8 || ^9 || ^10 || ^11
+core_version_requirement: ^10.1 || ^11 || ^12
 
 dependencies:
   - drupal:node
diff --git a/nodeaccess.install b/nodeaccess.install
index 90043fa..ff4779f 100644
--- a/nodeaccess.install
+++ b/nodeaccess.install
@@ -154,9 +154,9 @@ function nodeaccess_update_9002(&$sandbox) {
   $old_grants = $config->get('grants');
   $config
     ->set('allowed_grant_operations', [
-      'grant_view' => (boolean) $old_grants['view'],
-      'grant_update' => (boolean) $old_grants['edit'],
-      'grant_delete' => (boolean) $old_grants['delete'],
+      'grant_view' => (bool) $old_grants['view'],
+      'grant_update' => (bool) $old_grants['edit'],
+      'grant_delete' => (bool) $old_grants['delete'],
     ])
     ->clear('grants');
 
@@ -164,7 +164,7 @@ function nodeaccess_update_9002(&$sandbox) {
   $old_allowed_types = $config->get('allowed_types') ?? [];
   $grants_tab_availability = [];
   foreach ($old_allowed_types as $bundle => $value) {
-    $grants_tab_availability[$bundle] = (boolean) $value;
+    $grants_tab_availability[$bundle] = (bool) $value;
   }
   $config
     ->set('grants_tab_availability', $grants_tab_availability)
@@ -184,7 +184,7 @@ function nodeaccess_update_9002(&$sandbox) {
       'display_name' => $value['alias'],
       'name' => $value['name'],
       'weight' => (int) $value['weight'],
-      'selected' => (boolean) $value['allow'],
+      'selected' => (bool) $value['allow'],
     ];
   }
   $config
diff --git a/nodeaccess.module b/nodeaccess.module
index a193852..392bc91 100644
--- a/nodeaccess.module
+++ b/nodeaccess.module
@@ -5,9 +5,10 @@
  * Control access to site content based on the users and roles.
  */
 
+use Drupal\Core\Hook\Attribute\LegacyHook;
+use Drupal\nodeaccess\Hook\NodeaccessHooks;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\Core\Session\AccountInterface;
-use Drupal\user\Entity\Role;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\node\NodeTypeInterface;
 use Drupal\node\NodeInterface;
@@ -15,232 +16,71 @@ use Drupal\node\NodeInterface;
 /**
  * Implements hook_help().
  */
+#[LegacyHook]
 function nodeaccess_help($route_name, RouteMatchInterface $route_match) {
-  switch ($route_name) {
-    case 'entity.node.grants':
-      return t("You can set grants for individual users. Enter a name or a partial name in the box and click Search or press return. You must check the 'Keep?' checkbox if you want to keep the user for granting. Note that user grants are in addition to those coming from roles.");
-  }
+  return \Drupal::service(NodeaccessHooks::class)->help($route_name, $route_match);
 }
 
 /**
  * Implements hook_node_access_records().
  */
+#[LegacyHook]
 function nodeaccess_node_access_records(NodeInterface $node) {
-  $grants = [];
-  $settings = \Drupal::configFactory()->get('nodeaccess.settings');
-  $bundles_roles_grants = $settings->get('bundles_roles_grants');
-  $bundle_roles_grants = $bundles_roles_grants[$node->bundle()] ?? [];
-
-  // `nodeaccess_author` grant is loaded from `nodeaccess.settings`.
-  if (isset($bundle_roles_grants['author'])) {
-    // If the node's author is anonymous, ignore. Don't grant anonymous the
-    // author access to avoid unexpected permission leaks.
-    // @todo Add a test.
-    if ($node->getOwner() && $node->getOwner()->isAuthenticated()) {
-      $grants[] = [
-        'realm' => 'nodeaccess_author',
-        'gid' => $node->getOwnerId(),
-      ] + $bundle_roles_grants['author'];
-    }
-    unset($bundle_roles_grants['author']);
-  }
-
-  $map_rid_gid = $settings->get('map_rid_gid');
-
-  // Load grants from the `nodeaccess` table.
-  $db = \Drupal::database();
-  $entries = $db->select('nodeaccess', 'n')
-    ->fields('n')
-    ->condition('nid', $node->id())
-    ->execute()
-    ->fetchAll();
-
-  // If grants found in the `nodeaccess` table, it means that this node has its
-  // own grants granted via the Grants tab, no need to load grants from the
-  // `nodeaccess.settings`.
-  if (!empty($entries)) {
-    foreach ($entries as $row) {
-      $grant_languages = $node->getTranslationLanguages(TRUE);
-      foreach ($grant_languages as $grant_language) {
-        $translation = $node->getTranslation($grant_language->getId());
-        $translation_published = $translation->isPublished() ? 1 : 0;
-        $grants[] = [
-          'realm' => $row->realm,
-          'gid' => $row->gid,
-          'grant_view' => $row->grant_view ? $translation_published : 0,
-          'grant_update' => $row->grant_update,
-          'grant_delete' => $row->grant_delete,
-          'langcode' => $grant_language->getId(),
-        ];
-      }
-    }
-
-    // Ensure anonymous users have grants.
-    $anonymous_rid = AccountInterface::ANONYMOUS_ROLE;
-    $map_rid_gid = $settings->get('map_rid_gid');
-
-    if (isset($map_rid_gid[$anonymous_rid])) {
-      // Only filter out grants for anonymous if no valid grants present.
-      $anonymous_grants = array_filter($grants, fn($grant) => $grant['gid'] === $map_rid_gid[$anonymous_rid]);
-
-      if (empty($anonymous_grants)) {
-        // Add strict default grants for anonymous if none exist.
-        $grants[] = [
-          'realm' => 'nodeaccess_role',
-          'gid' => $map_rid_gid[$anonymous_rid],
-          'grant_view' => 0,
-          'grant_update' => 0,
-          'grant_delete' => 0,
-        ];
-      }
-    }
-
-    return $grants;
-  }
-
-  // Or load grants from `nodeaccess.settings`.
-  if (!empty($bundle_roles_grants)) {
-    foreach ($bundle_roles_grants as $role_id => $grant_values) {
-      // $grant_values holds values with key grant_view, grant_update and
-      // grant_delete.
-      $grant_languages = $node->getTranslationLanguages(TRUE);
-      foreach ($grant_languages as $grant_language) {
-        $translation = $node->getTranslation($grant_language->getId());
-        $translation_published = $translation->isPublished() ? 1 : 0;
-        $grants[] = [
-          'realm' => 'nodeaccess_role',
-          'gid' => $map_rid_gid[$role_id],
-          'grant_view' => $grant_values['grant_view'] ? $translation_published : 0,
-          'grant_update' => $grant_values['grant_update'],
-          'grant_delete' => $grant_values['grant_delete'],
-          'langcode' => $grant_language->getId(),
-        ];
-      }
-    }
-  }
-  return $grants;
+  return \Drupal::service(NodeaccessHooks::class)->nodeAccessRecords($node);
 }
 
 /**
  * Implements hook_node_grants().
  */
+#[LegacyHook]
 function nodeaccess_node_grants(AccountInterface $account, $op) {
-  $config = \Drupal::configFactory()->get('nodeaccess.settings');
-  $map_rid_gid = $config->get('map_rid_gid');
-  $roles = $account->getRoles();
-  $grant_ids = [];
-  if (is_array($map_rid_gid)) {
-    foreach ($roles as $role_id) {
-      if (isset($map_rid_gid[$role_id])) {
-        $grant_ids[] = $map_rid_gid[$role_id];
-      }
-    }
-  }
-  $user_id = $account->id();
-  if ($account->isAnonymous()) {
-    return [
-      'nodeaccess_role' => $grant_ids,
-      'nodeaccess_user' => [$user_id],
-    ];
-  }
-  return [
-    'nodeaccess_role' => $grant_ids,
-    'nodeaccess_user' => [$user_id],
-    'nodeaccess_author' => [$user_id],
-  ];
+  return \Drupal::service(NodeaccessHooks::class)->nodeGrants($account, $op);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_insert().
  */
+#[LegacyHook]
 function nodeaccess_node_type_insert(NodeTypeInterface $node_type) {
-  // Update `grants_tab_availability` and `bundles_roles_grants` in
-  // `nodeaccess.settings` to add this bundle related settings.
-  $bundle = $node_type->id();
-  $nodeaccess_settings = \Drupal::configFactory()->getEditable('nodeaccess.settings');
-
-  $bundle_roles_grants = [];
-  foreach (Role::loadMultiple() as $role_id => $role) {
-    $bundle_roles_grants[$role_id] = [
-      'grant_view' => 0,
-      'grant_update' => 0,
-      'grant_delete' => 0,
-    ];
-  }
-  $bundle_roles_grants['author'] = [
-    'grant_view' => 0,
-    'grant_update' => 0,
-    'grant_delete' => 0,
-  ];
-  $grants_tab_availability = $nodeaccess_settings->get('grants_tab_availability');
-  $grants_tab_availability[$bundle] = FALSE;
-  $bundles_roles_grants = $nodeaccess_settings->get('bundles_roles_grants');
-  $bundles_roles_grants[$bundle] = $bundle_roles_grants;
-  $nodeaccess_settings
-    ->set('grants_tab_availability', $grants_tab_availability)
-    ->set('bundles_roles_grants', $bundles_roles_grants)
-    ->save();
-  // @todo display a message or something to promote end users to update
-  //   `nodeaccess.settings`.
-  node_access_needs_rebuild(TRUE);
+  \Drupal::service(NodeaccessHooks::class)->nodeTypeInsert($node_type);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_delete().
  */
+#[LegacyHook]
 function nodeaccess_node_type_delete(NodeTypeInterface $node_type) {
-  // Update `grants_tab_availability` and `bundles_roles_grants` in
-  // `nodeaccess.settings` to remove this bundle related settings.
-  $bundle = $node_type->id();
-  $nodeaccess_settings = \Drupal::configFactory()->getEditable('nodeaccess.settings');
-  $grants_tab_availability = $nodeaccess_settings->get('grants_tab_availability');
-  $bundles_roles_grants = $nodeaccess_settings->get('bundles_roles_grants');
-  unset($grants_tab_availability[$bundle]);
-  unset($bundles_roles_grants[$bundle]);
-  $nodeaccess_settings
-    ->set('grants_tab_availability', $grants_tab_availability)
-    ->set('bundles_roles_grants', $bundles_roles_grants)
-    ->save();
+  \Drupal::service(NodeaccessHooks::class)->nodeTypeDelete($node_type);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_delete().
  */
+#[LegacyHook]
 function nodeaccess_node_delete(EntityInterface $node) {
-  // Remove related records from the `nodeaccess` table .
-  \Drupal::database()->delete('nodeaccess')
-    ->condition('nid', $node->id())
-    ->execute();
+  \Drupal::service(NodeaccessHooks::class)->nodeDelete($node);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_insert().
  */
+#[LegacyHook]
 function nodeaccess_user_role_insert(EntityInterface $entity) {
-  /** @var \Drupal\user\RoleInterface $entity */
-  /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
-  $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
-  $nodeaccess_helper->addRoleRelatedSettings($entity);
+  \Drupal::service(NodeaccessHooks::class)->userRoleInsert($entity);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_update().
  */
+#[LegacyHook]
 function nodeaccess_user_role_update(EntityInterface $entity) {
-  /** @var \Drupal\user\RoleInterface $entity */
-  /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
-  $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
-  $nodeaccess_helper->updateRoleRelatedSettings($entity);
+  \Drupal::service(NodeaccessHooks::class)->userRoleUpdate($entity);
 }
 
 /**
  * Implements hook_ENTITY_TYPE_delete().
  */
+#[LegacyHook]
 function nodeaccess_user_role_delete(EntityInterface $entity) {
-  /** @var \Drupal\user\RoleInterface $entity */
-  /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
-  $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
-  $nodeaccess_helper->deleteRoleRelatedSettings($entity);
-  // Rebuild permissions after deleting role-related settings.
-  \Drupal::service('router.builder')->rebuild();
+  \Drupal::service(NodeaccessHooks::class)->userRoleDelete($entity);
 }
diff --git a/nodeaccess.services.yml b/nodeaccess.services.yml
index 0267abb..2817fb2 100644
--- a/nodeaccess.services.yml
+++ b/nodeaccess.services.yml
@@ -2,3 +2,7 @@ services:
   nodeaccess.helper:
     class: Drupal\nodeaccess\NodeAccessHelper
     arguments: ['@database', '@entity_type.manager', '@config.factory']
+
+  Drupal\nodeaccess\Hook\NodeaccessHooks:
+    class: Drupal\nodeaccess\Hook\NodeaccessHooks
+    autowire: true
diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php
index 230c843..3f169ac 100644
--- a/src/Form/SettingsForm.php
+++ b/src/Form/SettingsForm.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\nodeaccess\Form;
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\node\NodeAccessRebuild;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -253,7 +255,7 @@ final class SettingsForm extends ConfigFormBase {
       ->set('grants_tab_availability', $grants_tab_availability)
       ->set('roles_settings', $values['roles_settings']['settings'] ?? [])
       ->save();
-    node_access_needs_rebuild(TRUE);
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->setNeedsRebuild(TRUE), fn() => node_access_needs_rebuild(TRUE));
     parent::submitForm($form, $form_state);
   }
 
diff --git a/src/Hook/NodeaccessHooks.php b/src/Hook/NodeaccessHooks.php
new file mode 100644
index 0000000..7c9b5a0
--- /dev/null
+++ b/src/Hook/NodeaccessHooks.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace Drupal\nodeaccess\Hook;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\node\NodeAccessRebuild;
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\user\Entity\Role;
+use Drupal\node\NodeTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Core\Routing\RouteMatchInterface;
+use Drupal\Core\Hook\Attribute\Hook;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+
+/**
+ * Hook implementations for nodeaccess.
+ */
+class NodeaccessHooks {
+  use StringTranslationTrait;
+
+  /**
+   * Implements hook_help().
+   */
+  #[Hook('help')]
+  public function help($route_name, RouteMatchInterface $route_match) {
+    switch ($route_name) {
+      case 'entity.node.grants':
+        return $this->t("You can set grants for individual users. Enter a name or a partial name in the box and click Search or press return. You must check the 'Keep?' checkbox if you want to keep the user for granting. Note that user grants are in addition to those coming from roles.");
+    }
+  }
+
+  /**
+   * Implements hook_node_access_records().
+   */
+  #[Hook('node_access_records')]
+  public function nodeAccessRecords(NodeInterface $node) {
+    $grants = [];
+    $settings = \Drupal::configFactory()->get('nodeaccess.settings');
+    $bundles_roles_grants = $settings->get('bundles_roles_grants');
+    $bundle_roles_grants = $bundles_roles_grants[$node->bundle()] ?? [];
+    // `nodeaccess_author` grant is loaded from `nodeaccess.settings`.
+    if (isset($bundle_roles_grants['author'])) {
+      // If the node's author is anonymous, ignore. Don't grant anonymous the
+      // author access to avoid unexpected permission leaks.
+      // @todo Add a test.
+      if ($node->getOwner() && $node->getOwner()->isAuthenticated()) {
+        $grants[] = [
+          'realm' => 'nodeaccess_author',
+          'gid' => $node->getOwnerId(),
+        ] + $bundle_roles_grants['author'];
+      }
+      unset($bundle_roles_grants['author']);
+    }
+    $map_rid_gid = $settings->get('map_rid_gid');
+    // Load grants from the `nodeaccess` table.
+    $db = \Drupal::database();
+    $entries = $db->select('nodeaccess', 'n')->fields('n')->condition('nid', $node->id())->execute()->fetchAll();
+    // If grants found in the `nodeaccess` table, it means that this node has its
+    // own grants granted via the Grants tab, no need to load grants from the
+    // `nodeaccess.settings`.
+    if (!empty($entries)) {
+      foreach ($entries as $row) {
+        $grant_languages = $node->getTranslationLanguages(TRUE);
+        foreach ($grant_languages as $grant_language) {
+          $translation = $node->getTranslation($grant_language->getId());
+          $translation_published = $translation->isPublished() ? 1 : 0;
+          $grants[] = [
+            'realm' => $row->realm,
+            'gid' => $row->gid,
+            'grant_view' => $row->grant_view ? $translation_published : 0,
+            'grant_update' => $row->grant_update,
+            'grant_delete' => $row->grant_delete,
+            'langcode' => $grant_language->getId(),
+          ];
+        }
+      }
+      // Ensure anonymous users have grants.
+      $anonymous_rid = AccountInterface::ANONYMOUS_ROLE;
+      $map_rid_gid = $settings->get('map_rid_gid');
+      if (isset($map_rid_gid[$anonymous_rid])) {
+        // Only filter out grants for anonymous if no valid grants present.
+        $anonymous_grants = array_filter($grants, fn($grant) => $grant['gid'] === $map_rid_gid[$anonymous_rid]);
+        if (empty($anonymous_grants)) {
+          // Add strict default grants for anonymous if none exist.
+          $grants[] = [
+            'realm' => 'nodeaccess_role',
+            'gid' => $map_rid_gid[$anonymous_rid],
+            'grant_view' => 0,
+            'grant_update' => 0,
+            'grant_delete' => 0,
+          ];
+        }
+      }
+      return $grants;
+    }
+    // Or load grants from `nodeaccess.settings`.
+    if (!empty($bundle_roles_grants)) {
+      foreach ($bundle_roles_grants as $role_id => $grant_values) {
+        // $grant_values holds values with key grant_view, grant_update and
+        // grant_delete.
+        $grant_languages = $node->getTranslationLanguages(TRUE);
+        foreach ($grant_languages as $grant_language) {
+          $translation = $node->getTranslation($grant_language->getId());
+          $translation_published = $translation->isPublished() ? 1 : 0;
+          $grants[] = [
+            'realm' => 'nodeaccess_role',
+            'gid' => $map_rid_gid[$role_id],
+            'grant_view' => $grant_values['grant_view'] ? $translation_published : 0,
+            'grant_update' => $grant_values['grant_update'],
+            'grant_delete' => $grant_values['grant_delete'],
+            'langcode' => $grant_language->getId(),
+          ];
+        }
+      }
+    }
+    return $grants;
+  }
+
+  /**
+   * Implements hook_node_grants().
+   */
+  #[Hook('node_grants')]
+  public function nodeGrants(AccountInterface $account, $op) {
+    $config = \Drupal::configFactory()->get('nodeaccess.settings');
+    $map_rid_gid = $config->get('map_rid_gid');
+    $roles = $account->getRoles();
+    $grant_ids = [];
+    if (is_array($map_rid_gid)) {
+      foreach ($roles as $role_id) {
+        if (isset($map_rid_gid[$role_id])) {
+          $grant_ids[] = $map_rid_gid[$role_id];
+        }
+      }
+    }
+    $user_id = $account->id();
+    if ($account->isAnonymous()) {
+      return [
+        'nodeaccess_role' => $grant_ids,
+        'nodeaccess_user' => [
+          $user_id,
+        ],
+      ];
+    }
+    return [
+      'nodeaccess_role' => $grant_ids,
+      'nodeaccess_user' => [
+        $user_id,
+      ],
+      'nodeaccess_author' => [
+        $user_id,
+      ],
+    ];
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('node_type_insert')]
+  public function nodeTypeInsert(NodeTypeInterface $node_type) {
+    // Update `grants_tab_availability` and `bundles_roles_grants` in
+    // `nodeaccess.settings` to add this bundle related settings.
+    $bundle = $node_type->id();
+    $nodeaccess_settings = \Drupal::configFactory()->getEditable('nodeaccess.settings');
+    $bundle_roles_grants = [];
+    foreach (Role::loadMultiple() as $role_id => $role) {
+      $bundle_roles_grants[$role_id] = [
+        'grant_view' => 0,
+        'grant_update' => 0,
+        'grant_delete' => 0,
+      ];
+    }
+    $bundle_roles_grants['author'] = [
+      'grant_view' => 0,
+      'grant_update' => 0,
+      'grant_delete' => 0,
+    ];
+    $grants_tab_availability = $nodeaccess_settings->get('grants_tab_availability');
+    $grants_tab_availability[$bundle] = FALSE;
+    $bundles_roles_grants = $nodeaccess_settings->get('bundles_roles_grants');
+    $bundles_roles_grants[$bundle] = $bundle_roles_grants;
+    $nodeaccess_settings->set('grants_tab_availability', $grants_tab_availability)->set('bundles_roles_grants', $bundles_roles_grants)->save();
+    // @todo display a message or something to promote end users to update
+    //   `nodeaccess.settings`.
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->setNeedsRebuild(TRUE), fn() => node_access_needs_rebuild(TRUE));
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete().
+   */
+  #[Hook('node_type_delete')]
+  public function nodeTypeDelete(NodeTypeInterface $node_type) {
+    // Update `grants_tab_availability` and `bundles_roles_grants` in
+    // `nodeaccess.settings` to remove this bundle related settings.
+    $bundle = $node_type->id();
+    $nodeaccess_settings = \Drupal::configFactory()->getEditable('nodeaccess.settings');
+    $grants_tab_availability = $nodeaccess_settings->get('grants_tab_availability');
+    $bundles_roles_grants = $nodeaccess_settings->get('bundles_roles_grants');
+    unset($grants_tab_availability[$bundle]);
+    unset($bundles_roles_grants[$bundle]);
+    $nodeaccess_settings->set('grants_tab_availability', $grants_tab_availability)->set('bundles_roles_grants', $bundles_roles_grants)->save();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete().
+   */
+  #[Hook('node_delete')]
+  public function nodeDelete(EntityInterface $node) {
+    // Remove related records from the `nodeaccess` table .
+    \Drupal::database()->delete('nodeaccess')->condition('nid', $node->id())->execute();
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_insert().
+   */
+  #[Hook('user_role_insert')]
+  public function userRoleInsert(EntityInterface $entity) {
+    /** @var \Drupal\user\RoleInterface $entity */
+    /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
+    $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
+    $nodeaccess_helper->addRoleRelatedSettings($entity);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_update().
+   */
+  #[Hook('user_role_update')]
+  public function userRoleUpdate(EntityInterface $entity) {
+    /** @var \Drupal\user\RoleInterface $entity */
+    /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
+    $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
+    $nodeaccess_helper->updateRoleRelatedSettings($entity);
+  }
+
+  /**
+   * Implements hook_ENTITY_TYPE_delete().
+   */
+  #[Hook('user_role_delete')]
+  public function userRoleDelete(EntityInterface $entity) {
+    /** @var \Drupal\user\RoleInterface $entity */
+    /** @var \Drupal\nodeaccess\NodeAccessHelper $nodeaccess_helper */
+    $nodeaccess_helper = \Drupal::service('nodeaccess.helper');
+    $nodeaccess_helper->deleteRoleRelatedSettings($entity);
+    // Rebuild permissions after deleting role-related settings.
+    \Drupal::service('router.builder')->rebuild();
+  }
+
+}
diff --git a/src/NodeAccessHelper.php b/src/NodeAccessHelper.php
index fde8c41..e41623c 100644
--- a/src/NodeAccessHelper.php
+++ b/src/NodeAccessHelper.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\nodeaccess;
 
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\Core\Database\Statement\FetchAs;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Config\ImmutableConfig;
 use Drupal\Core\Database\Connection;
@@ -74,22 +76,28 @@ class NodeAccessHelper {
     $grant_ids = array_map(function ($role_id) use ($map_rid_gid) {
       return $map_rid_gid[$role_id];
     }, $role_ids);
-    $results = $this->database->select('node_access', 'n')
+    $results = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $this->database->select('node_access', 'n')
       ->fields('n', ['gid', 'grant_view', 'grant_update', 'grant_delete'])
       ->condition('n.realm', 'nodeaccess_role',)
       ->condition('n.gid', $grant_ids, 'IN')
       ->condition('n.nid', "$nid")
       ->execute()
-      ->fetchAllAssoc('gid', \PDO::FETCH_ASSOC);
+      ->fetchAllAssoc('gid', FetchAs::Associative), fn() => $this->database->select('node_access', 'n')
+      ->fields('n', ['gid', 'grant_view', 'grant_update', 'grant_delete'])
+      ->condition('n.realm', 'nodeaccess_role',)
+      ->condition('n.gid', $grant_ids, 'IN')
+      ->condition('n.nid', "$nid")
+      ->execute()
+      ->fetchAllAssoc('gid', \PDO::FETCH_ASSOC));
 
     // $roles_settings = $nodeaccess_settings->get('roles_settings');
     // $map_gid_rid = array_flip($map_rid_gid);
     foreach ($grant_ids as $grant_id) {
       // Convert to boolean for later use in form.
       $grants[$grant_id] = [
-        'grant_view' => (boolean) ($results[$grant_id]['grant_view'] ?? FALSE),
-        'grant_update' => (boolean) ($results[$grant_id]['grant_update'] ?? FALSE),
-        'grant_delete' => (boolean) ($results[$grant_id]['grant_delete'] ?? FALSE),
+        'grant_view' => (bool) ($results[$grant_id]['grant_view'] ?? FALSE),
+        'grant_update' => (bool) ($results[$grant_id]['grant_update'] ?? FALSE),
+        'grant_delete' => (bool) ($results[$grant_id]['grant_delete'] ?? FALSE),
       ];
     }
     return $grants;
@@ -107,20 +115,25 @@ class NodeAccessHelper {
   public function loadUsersGrants($nid) {
     // Load users from node_access.
     $grants = [];
-    $results = $this->database->select('node_access', 'n')
+    $results = DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.2.0', fn() => $this->database->select('node_access', 'n')
+      ->fields('n', ['gid', 'grant_view', 'grant_update', 'grant_delete'])
+      ->condition('n.nid', "$nid")
+      ->condition('n.realm', 'nodeaccess_user')
+      ->execute()
+      ->fetchAllAssoc('gid', FetchAs::Associative), fn() => $this->database->select('node_access', 'n')
       ->fields('n', ['gid', 'grant_view', 'grant_update', 'grant_delete'])
       ->condition('n.nid', "$nid")
       ->condition('n.realm', 'nodeaccess_user')
       ->execute()
-      ->fetchAllAssoc('gid', \PDO::FETCH_ASSOC);
+      ->fetchAllAssoc('gid', \PDO::FETCH_ASSOC));
     // The gid is a user ID.
     foreach ($results as $gid => $grant) {
       // Convert to boolean for later use in form.
       $grants[$gid] = [
         'keep' => 1,
-        'grant_view' => (boolean) $grant['grant_view'],
-        'grant_update' => (boolean) $grant['grant_update'],
-        'grant_delete' => (boolean) $grant['grant_delete'],
+        'grant_view' => (bool) $grant['grant_view'],
+        'grant_update' => (bool) $grant['grant_update'],
+        'grant_delete' => (bool) $grant['grant_delete'],
       ];
     }
     return $grants;
@@ -172,7 +185,7 @@ class NodeAccessHelper {
         ->countQuery()
         ->execute()
         ->fetchField();
-      $grant[$grant_type] = (boolean) $count;
+      $grant[$grant_type] = (bool) $count;
     }
     return $grant;
   }
diff --git a/tests/src/Functional/GrantsFormTest.php b/tests/src/Functional/GrantsFormTest.php
index 58f4557..c10f90e 100644
--- a/tests/src/Functional/GrantsFormTest.php
+++ b/tests/src/Functional/GrantsFormTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\nodeaccess\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\Core\Url;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
@@ -14,6 +16,8 @@ use Drupal\user\Entity\Role;
  * @group nodeaccess
  * @covers \Drupal\nodeaccess\Form\SettingsForm
  */
+#[Group('nodeaccess')]
+#[RunTestsInSeparateProcesses]
 class GrantsFormTest extends BrowserTestBase {
 
   use ContentTypeCreationTrait;
diff --git a/tests/src/Functional/NodeAccessTest.php b/tests/src/Functional/NodeAccessTest.php
index f818d88..edb6e69 100644
--- a/tests/src/Functional/NodeAccessTest.php
+++ b/tests/src/Functional/NodeAccessTest.php
@@ -2,6 +2,10 @@
 
 namespace Drupal\Tests\nodeaccess\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
+use Drupal\Component\Utility\DeprecationHelper;
+use Drupal\node\NodeAccessRebuild;
 use Drupal\Core\Url;
 use Drupal\language\Entity\ConfigurableLanguage;
 use Drupal\Tests\BrowserTestBase;
@@ -14,6 +18,8 @@ use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
  * @covers \Drupal\nodeaccess\Form\SettingsForm
  * @covers \Drupal\nodeaccess\Form\GrantsForm
  */
+#[Group('nodeaccess')]
+#[RunTestsInSeparateProcesses]
 class NodeAccessTest extends BrowserTestBase {
 
   use ContentTypeCreationTrait;
@@ -92,7 +98,7 @@ class NodeAccessTest extends BrowserTestBase {
     $assert_session->pageTextContains('Nodeaccess settings');
     $this->submitForm(['bundles_roles_grants[foo][settings][author][grant_view]' => 1], 'Save configuration');
     $this->assertSession()->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
 
     $node = $this->drupalCreateNode(['type' => 'foo']);
     $node_view_url = $node->toUrl()->toString();
@@ -116,7 +122,7 @@ class NodeAccessTest extends BrowserTestBase {
       'bundles_roles_grants[foo][settings][author][grant_delete]' => 1,
     ], 'Save configuration');
     $this->assertSession()->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
     $this->drupalGet($node_view_url);
     $assert_session->statusCodeEquals(403);
     $this->drupalGet($node_edit_url);
@@ -150,7 +156,7 @@ class NodeAccessTest extends BrowserTestBase {
       "roles_settings[settings][$admin_role_id][selected]" => 1,
     ], 'Save configuration');
     $assert_session->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
 
     $node = $this->drupalCreateNode(['type' => 'foo']);
     $node_view_url = $node->toUrl()->toString();
@@ -193,7 +199,7 @@ class NodeAccessTest extends BrowserTestBase {
       "roles_settings[settings][anonymous][selected]" => 1,
     ], 'Save configuration');
     $assert_session->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
     $node = $this->drupalCreateNode(['type' => 'foo']);
     $node_view_url = $node->toUrl()->toString();
     $node_edit_url = $node->toUrl('edit-form')->toString();
@@ -255,7 +261,7 @@ class NodeAccessTest extends BrowserTestBase {
       "roles_settings[settings][authenticated][selected]" => 1,
     ], 'Save configuration');
     $assert_session->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
     $node = $this->drupalCreateNode(['type' => 'foo']);
     $node_view_url = $node->toUrl()->toString();
     $node_edit_url = $node->toUrl('edit-form')->toString();
@@ -314,7 +320,7 @@ class NodeAccessTest extends BrowserTestBase {
     $assert_session->pageTextContains('Nodeaccess settings');
     $this->submitForm(['bundles_roles_grants[foo][settings][author][grant_view]' => 1], 'Save configuration');
     $this->assertSession()->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
 
     $this->drupalGet($node_view_url);
     $assert_session->pageTextContains($node->label());
@@ -331,7 +337,7 @@ class NodeAccessTest extends BrowserTestBase {
       'bundles_roles_grants[foo][settings][author][grant_delete]' => 1,
     ], 'Save configuration');
     $this->assertSession()->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
     $this->drupalGet($node_view_url);
     $assert_session->statusCodeEquals(403);
     $this->drupalGet($node_edit_url);
@@ -357,7 +363,7 @@ class NodeAccessTest extends BrowserTestBase {
       "roles_settings[settings][anonymous][selected]" => 1,
     ], 'Save configuration');
     $assert_session->pageTextContains('The configuration options have been saved.');
-    node_access_rebuild();
+    DeprecationHelper::backwardsCompatibleCall(\Drupal::VERSION, '11.4.0', fn() => \Drupal::service(NodeAccessRebuild::class)->rebuild(), fn() => node_access_rebuild());
     $node = $this->drupalCreateNode(['type' => 'foo']);
     $node->setUnpublished()->save();
     $this->assertFalse($node->isPublished());
diff --git a/tests/src/Functional/SettingsFormTest.php b/tests/src/Functional/SettingsFormTest.php
index 3508ed6..73b4f7b 100644
--- a/tests/src/Functional/SettingsFormTest.php
+++ b/tests/src/Functional/SettingsFormTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\nodeaccess\Functional;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\node\Entity\NodeType;
 use Drupal\Tests\BrowserTestBase;
 use Drupal\Tests\node\Traits\ContentTypeCreationTrait;
@@ -13,6 +15,8 @@ use Drupal\user\Entity\Role;
  * @group nodeaccess
  * @covers \Drupal\nodeaccess\Form\SettingsForm
  */
+#[Group('nodeaccess')]
+#[RunTestsInSeparateProcesses]
 class SettingsFormTest extends BrowserTestBase {
 
   use ContentTypeCreationTrait;
diff --git a/tests/src/Kernel/NodeGrantPermissionsTest.php b/tests/src/Kernel/NodeGrantPermissionsTest.php
index b640dd6..dd081f7 100644
--- a/tests/src/Kernel/NodeGrantPermissionsTest.php
+++ b/tests/src/Kernel/NodeGrantPermissionsTest.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\Tests\nodeaccess\Kernel;
 
+use PHPUnit\Framework\Attributes\Group;
+use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses;
 use Drupal\KernelTests\KernelTestBase;
 use Drupal\node\Entity\NodeType;
 use Drupal\user\Entity\Role;
@@ -11,6 +13,8 @@ use Drupal\user\Entity\Role;
  *
  * @group nodeaccess
  */
+#[Group('nodeaccess')]
+#[RunTestsInSeparateProcesses]
 class NodeGrantPermissionsTest extends KernelTestBase {
 
   /**
