diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts
index 6469279f8c6cdfc6089b7075a427d97620650ce2..d7619f3ac6c2ec700fd3855210e46f50d3cd0cd6 100644
--- a/lib/datasource/api.ts
+++ b/lib/datasource/api.ts
@@ -30,7 +30,7 @@ import * as pod from './pod';
 import { PypiDatasource } from './pypi';
 import * as repology from './repology';
 import { RubyVersionDatasource } from './ruby-version';
-import * as rubygems from './rubygems';
+import { RubyGemsDatasource } from './rubygems';
 import * as sbtPackage from './sbt-package';
 import * as sbtPlugin from './sbt-plugin';
 import { TerraformModuleDatasource } from './terraform-module';
@@ -72,7 +72,7 @@ api.set('pod', pod);
 api.set('pypi', new PypiDatasource());
 api.set('repology', repology);
 api.set('ruby-version', new RubyVersionDatasource());
-api.set('rubygems', rubygems);
+api.set(RubyGemsDatasource.id, new RubyGemsDatasource());
 api.set('sbt-package', sbtPackage);
 api.set('sbt-plugin', sbtPlugin);
 api.set('terraform-module', new TerraformModuleDatasource());
diff --git a/lib/datasource/rubygems/common.ts b/lib/datasource/rubygems/common.ts
deleted file mode 100644
index ce52a088a1e9c6f278fa0ca01eb3690a64ba950a..0000000000000000000000000000000000000000
--- a/lib/datasource/rubygems/common.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import Marshal from 'marshal';
-import urlJoin from 'url-join';
-import { logger } from '../../logger';
-import { Http } from '../../util/http';
-import { getQueryString } from '../../util/url';
-
-export const id = 'rubygems';
-export const http = new Http(id);
-
-export const knownFallbackHosts = ['rubygems.pkg.github.com', 'gitlab.com'];
-
-export async function fetchJson<T>(
-  dependency: string,
-  registry: string,
-  path: string
-): Promise<T> {
-  const url = urlJoin(registry, path, `${dependency}.json`);
-
-  logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
-  const response = (await http.getJson<T>(url)) || {
-    body: undefined,
-  };
-
-  return response.body;
-}
-
-export async function fetchBuffer<T>(
-  dependency: string,
-  registry: string,
-  path: string
-): Promise<T> {
-  const url = `${urlJoin(registry, path)}?${getQueryString({
-    gems: dependency,
-  })}`;
-
-  logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
-  const response = await http.getBuffer(url);
-
-  return new Marshal(response.body).parsed as T;
-}
diff --git a/lib/datasource/rubygems/get-rubygems-org.ts b/lib/datasource/rubygems/get-rubygems-org.ts
index 7ca2bdcda8daf38b807adf07df0122f7298fe1d8..3be067768bda5c8ce004bf3a2adafeb57243fc0b 100644
--- a/lib/datasource/rubygems/get-rubygems-org.ts
+++ b/lib/datasource/rubygems/get-rubygems-org.ts
@@ -1,8 +1,8 @@
 import { logger } from '../../logger';
 import { ExternalHostError } from '../../types/errors/external-host-error';
 import { getElapsedMinutes } from '../../util/date';
-import type { ReleaseResult } from '../types';
-import { http } from './common';
+import { Datasource } from '../datasource';
+import type { GetReleasesConfig, ReleaseResult } from '../types';
 
 let lastSync = new Date('2000-01-01');
 let packageReleases: Record<string, string[]> = Object.create(null); // Because we might need a "constructor" key
@@ -15,38 +15,69 @@ export function resetCache(): void {
   contentLength = 0;
 }
 
-/* https://bugs.chromium.org/p/v8/issues/detail?id=2869 */
-const copystr = (x: string): string => (' ' + x).slice(1);
+export class RubyGemsOrgDatasource extends Datasource {
+  constructor(override readonly id: string) {
+    super(id);
+  }
 
-async function updateRubyGemsVersions(): Promise<void> {
-  const url = 'https://rubygems.org/versions';
-  const options = {
-    headers: {
-      'accept-encoding': 'identity',
-      range: `bytes=${contentLength}-`,
-    },
-  };
-  let newLines: string;
-  try {
-    logger.debug('Rubygems: Fetching rubygems.org versions');
-    const startTime = Date.now();
-    newLines = (await http.get(url, options)).body;
-    const durationMs = Math.round(Date.now() - startTime);
-    logger.debug({ durationMs }, 'Rubygems: Fetched rubygems.org versions');
-  } catch (err) /* istanbul ignore next */ {
-    if (err.statusCode !== 416) {
-      contentLength = 0;
-      packageReleases = Object.create(null); // Because we might need a "constructor" key
-      throw new ExternalHostError(
-        new Error('Rubygems fetch error - need to reset cache')
-      );
+  async getReleases({
+    lookupName,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    logger.debug(`getRubygemsOrgDependency(${lookupName})`);
+    await this.syncVersions();
+    if (!packageReleases[lookupName]) {
+      return null;
+    }
+    const dep: ReleaseResult = {
+      releases: packageReleases[lookupName].map((version) => ({
+        version,
+      })),
+    };
+    return dep;
+  }
+
+  /**
+   * https://bugs.chromium.org/p/v8/issues/detail?id=2869
+   */
+  private static copystr(x: string): string {
+    return (' ' + x).slice(1);
+  }
+
+  async updateRubyGemsVersions(): Promise<void> {
+    const url = 'https://rubygems.org/versions';
+    const options = {
+      headers: {
+        'accept-encoding': 'identity',
+        range: `bytes=${contentLength}-`,
+      },
+    };
+    let newLines: string;
+    try {
+      logger.debug('Rubygems: Fetching rubygems.org versions');
+      const startTime = Date.now();
+      newLines = (await this.http.get(url, options)).body;
+      const durationMs = Math.round(Date.now() - startTime);
+      logger.debug({ durationMs }, 'Rubygems: Fetched rubygems.org versions');
+    } catch (err) /* istanbul ignore next */ {
+      if (err.statusCode !== 416) {
+        contentLength = 0;
+        packageReleases = Object.create(null); // Because we might need a "constructor" key
+        throw new ExternalHostError(
+          new Error('Rubygems fetch error - need to reset cache')
+        );
+      }
+      logger.debug('Rubygems: No update');
+      lastSync = new Date();
+      return;
+    }
+
+    for (const line of newLines.split('\n')) {
+      RubyGemsOrgDatasource.processLine(line);
     }
-    logger.debug('Rubygems: No update');
     lastSync = new Date();
-    return;
   }
 
-  function processLine(line: string): void {
+  private static processLine(line: string): void {
     let split: string[];
     let pkg: string;
     let versions: string;
@@ -57,7 +88,7 @@ async function updateRubyGemsVersions(): Promise<void> {
       }
       split = l.split(' ');
       [pkg, versions] = split;
-      pkg = copystr(pkg);
+      pkg = RubyGemsOrgDatasource.copystr(pkg);
       packageReleases[pkg] = packageReleases[pkg] || [];
       const lineVersions = versions.split(',').map((version) => version.trim());
       for (const lineVersion of lineVersions) {
@@ -68,7 +99,7 @@ async function updateRubyGemsVersions(): Promise<void> {
             (version) => version !== deletedVersion
           );
         } else {
-          packageReleases[pkg].push(copystr(lineVersion));
+          packageReleases[pkg].push(RubyGemsOrgDatasource.copystr(lineVersion));
         }
       }
     } catch (err) /* istanbul ignore next */ {
@@ -79,38 +110,19 @@ async function updateRubyGemsVersions(): Promise<void> {
     }
   }
 
-  for (const line of newLines.split('\n')) {
-    processLine(line);
+  private static isDataStale(): boolean {
+    return getElapsedMinutes(lastSync) >= 5;
   }
-  lastSync = new Date();
-}
-
-function isDataStale(): boolean {
-  return getElapsedMinutes(lastSync) >= 5;
-}
 
-let updateRubyGemsVersionsPromise: Promise<void> | undefined;
+  updateRubyGemsVersionsPromise: Promise<void> | undefined;
 
-async function syncVersions(): Promise<void> {
-  if (isDataStale()) {
-    updateRubyGemsVersionsPromise =
-      // eslint-disable-next-line @typescript-eslint/no-misused-promises
-      updateRubyGemsVersionsPromise || updateRubyGemsVersions();
-    await updateRubyGemsVersionsPromise;
-    updateRubyGemsVersionsPromise = null;
-  }
-}
-
-export async function getRubygemsOrgDependency(
-  lookupName: string
-): Promise<ReleaseResult | null> {
-  logger.debug(`getRubygemsOrgDependency(${lookupName})`);
-  await syncVersions();
-  if (!packageReleases[lookupName]) {
-    return null;
+  async syncVersions(): Promise<void> {
+    if (RubyGemsOrgDatasource.isDataStale()) {
+      this.updateRubyGemsVersionsPromise =
+        // eslint-disable-next-line @typescript-eslint/no-misused-promises
+        this.updateRubyGemsVersionsPromise || this.updateRubyGemsVersions();
+      await this.updateRubyGemsVersionsPromise;
+      this.updateRubyGemsVersionsPromise = null;
+    }
   }
-  const dep: ReleaseResult = {
-    releases: packageReleases[lookupName].map((version) => ({ version })),
-  };
-  return dep;
 }
diff --git a/lib/datasource/rubygems/get.ts b/lib/datasource/rubygems/get.ts
index 6d1d491b198c2aa68469f1b7b8ff594fe301e3b3..a6b45f197ef8a9c3ec358bcc782cf98a6d6e8628 100644
--- a/lib/datasource/rubygems/get.ts
+++ b/lib/datasource/rubygems/get.ts
@@ -1,7 +1,9 @@
+import Marshal from 'marshal';
 import { logger } from '../../logger';
 import { HttpError } from '../../util/http/types';
-import type { Release, ReleaseResult } from '../types';
-import { fetchBuffer, fetchJson } from './common';
+import { getQueryString, joinUrlParts, parseUrl } from '../../util/url';
+import { Datasource } from '../datasource';
+import type { GetReleasesConfig, Release, ReleaseResult } from '../types';
 import type {
   JsonGemVersions,
   JsonGemsInfo,
@@ -12,114 +14,164 @@ const INFO_PATH = '/api/v1/gems';
 const VERSIONS_PATH = '/api/v1/versions';
 const DEPENDENCIES_PATH = '/api/v1/dependencies';
 
-export async function getDependencyFallback(
-  dependency: string,
-  registry: string
-): Promise<ReleaseResult | null> {
-  logger.debug(
-    { dependency, api: DEPENDENCIES_PATH },
-    'RubyGems lookup for dependency'
-  );
-  const info = await fetchBuffer<MarshalledVersionInfo[]>(
-    dependency,
-    registry,
-    DEPENDENCIES_PATH
-  );
-  if (!info || info.length === 0) {
-    return null;
+export class InternalRubyGemsDatasource extends Datasource {
+  constructor(override readonly id: string) {
+    super(id);
   }
-  const releases = info.map(({ number: version, platform: rubyPlatform }) => ({
-    version,
-    rubyPlatform,
-  }));
-  return {
-    releases,
-    homepage: null,
-    sourceUrl: null,
-    changelogUrl: null,
-  };
-}
 
-export async function getDependency(
-  dependency: string,
-  registry: string
-): Promise<ReleaseResult | null> {
-  logger.debug(
-    { dependency, api: INFO_PATH },
-    'RubyGems lookup for dependency'
-  );
-  let info: JsonGemsInfo;
-
-  try {
-    info = await fetchJson(dependency, registry, INFO_PATH);
-  } catch (error) {
-    // fallback to deps api on 404
-    if (error instanceof HttpError && error.response?.statusCode === 404) {
-      return await getDependencyFallback(dependency, registry);
+  private knownFallbackHosts = ['rubygems.pkg.github.com', 'gitlab.com'];
+
+  override getReleases({
+    lookupName,
+    registryUrl,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    if (this.knownFallbackHosts.includes(parseUrl(registryUrl)?.hostname)) {
+      return this.getDependencyFallback(lookupName, registryUrl);
     }
-    throw error;
+    return this.getDependency(lookupName, registryUrl);
   }
 
-  if (!info) {
-    logger.debug({ dependency }, 'RubyGems package not found.');
-    return null;
+  async getDependencyFallback(
+    dependency: string,
+    registry: string
+  ): Promise<ReleaseResult | null> {
+    logger.debug(
+      { dependency, api: DEPENDENCIES_PATH },
+      'RubyGems lookup for dependency'
+    );
+    const info = await this.fetchBuffer<MarshalledVersionInfo[]>(
+      dependency,
+      registry,
+      DEPENDENCIES_PATH
+    );
+    if (!info || info.length === 0) {
+      return null;
+    }
+    const releases = info.map(
+      ({ number: version, platform: rubyPlatform }) => ({
+        version,
+        rubyPlatform,
+      })
+    );
+    return {
+      releases,
+      homepage: null,
+      sourceUrl: null,
+      changelogUrl: null,
+    };
   }
 
-  if (dependency.toLowerCase() !== info.name.toLowerCase()) {
-    logger.warn(
-      { lookup: dependency, returned: info.name },
-      'Lookup name does not match with returned.'
+  async getDependency(
+    dependency: string,
+    registry: string
+  ): Promise<ReleaseResult | null> {
+    logger.debug(
+      { dependency, api: INFO_PATH },
+      'RubyGems lookup for dependency'
     );
-    return null;
-  }
+    let info: JsonGemsInfo;
+
+    try {
+      info = await this.fetchJson(dependency, registry, INFO_PATH);
+    } catch (error) {
+      // fallback to deps api on 404
+      if (error instanceof HttpError && error.response?.statusCode === 404) {
+        return await this.getDependencyFallback(dependency, registry);
+      }
+      throw error;
+    }
 
-  let versions: JsonGemVersions[] = [];
-  let releases: Release[] = [];
-  try {
-    versions = await fetchJson(dependency, registry, VERSIONS_PATH);
-  } catch (err) {
-    if (err.statusCode === 400 || err.statusCode === 404) {
-      logger.debug(
-        { registry },
-        'versions endpoint returns error - falling back to info endpoint'
+    if (!info) {
+      logger.debug({ dependency }, 'RubyGems package not found.');
+      return null;
+    }
+
+    if (dependency.toLowerCase() !== info.name.toLowerCase()) {
+      logger.warn(
+        { lookup: dependency, returned: info.name },
+        'Lookup name does not match with returned.'
       );
+      return null;
+    }
+
+    let versions: JsonGemVersions[] = [];
+    let releases: Release[] = [];
+    try {
+      versions = await this.fetchJson(dependency, registry, VERSIONS_PATH);
+    } catch (err) {
+      if (err.statusCode === 400 || err.statusCode === 404) {
+        logger.debug(
+          { registry },
+          'versions endpoint returns error - falling back to info endpoint'
+        );
+      } else {
+        throw err;
+      }
+    }
+
+    // TODO: invalid properties for `Release` see #11312
+
+    if (versions.length === 0 && info.version) {
+      logger.warn('falling back to the version from the info endpoint');
+      releases = [
+        {
+          version: info.version,
+          rubyPlatform: info.platform,
+        } as Release,
+      ];
     } else {
-      throw err;
+      releases = versions.map(
+        ({
+          number: version,
+          platform: rubyPlatform,
+          created_at: releaseTimestamp,
+          rubygems_version: rubygemsVersion,
+          ruby_version: rubyVersion,
+        }) => ({
+          version,
+          rubyPlatform,
+          releaseTimestamp,
+          rubygemsVersion,
+          rubyVersion,
+        })
+      );
     }
+
+    return {
+      releases,
+      homepage: info.homepage_uri,
+      sourceUrl: info.source_code_uri,
+      changelogUrl: info.changelog_uri,
+    };
   }
 
-  // TODO: invalid properties for `Release` see #11312
-
-  if (versions.length === 0 && info.version) {
-    logger.warn('falling back to the version from the info endpoint');
-    releases = [
-      {
-        version: info.version,
-        rubyPlatform: info.platform,
-      } as Release,
-    ];
-  } else {
-    releases = versions.map(
-      ({
-        number: version,
-        platform: rubyPlatform,
-        created_at: releaseTimestamp,
-        rubygems_version: rubygemsVersion,
-        ruby_version: rubyVersion,
-      }) => ({
-        version,
-        rubyPlatform,
-        releaseTimestamp,
-        rubygemsVersion,
-        rubyVersion,
-      })
-    );
+  private async fetchJson<T>(
+    dependency: string,
+    registry: string,
+    path: string
+  ): Promise<T> {
+    const url = joinUrlParts(registry, path, `${dependency}.json`);
+
+    logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
+    const response = (await this.http.getJson<T>(url)) || {
+      body: undefined,
+    };
+
+    return response.body;
   }
 
-  return {
-    releases,
-    homepage: info.homepage_uri,
-    sourceUrl: info.source_code_uri,
-    changelogUrl: info.changelog_uri,
-  };
+  private async fetchBuffer<T>(
+    dependency: string,
+    registry: string,
+    path: string
+  ): Promise<T> {
+    const url = `${joinUrlParts(registry, path)}?${getQueryString({
+      gems: dependency,
+    })}`;
+
+    logger.trace({ registry, dependency, url }, `RubyGems lookup request`);
+    const response = await this.http.getBuffer(url);
+
+    return new Marshal(response.body).parsed as T;
+  }
 }
diff --git a/lib/datasource/rubygems/index.spec.ts b/lib/datasource/rubygems/index.spec.ts
index 913204eafb2b5b527147842d8bd75eef649da788..71451f040f6febd2cf92f437a3b4ee52fd48d928 100644
--- a/lib/datasource/rubygems/index.spec.ts
+++ b/lib/datasource/rubygems/index.spec.ts
@@ -7,7 +7,7 @@ import {
 } from '../../../test/util';
 import * as rubyVersioning from '../../versioning/ruby';
 import { resetCache } from './get-rubygems-org';
-import * as rubygems from '.';
+import { RubyGemsDatasource } from '.';
 
 const rubygemsOrgVersions = loadFixture('rubygems-org.txt');
 const railsInfo = loadJsonFixture('rails/info.json');
@@ -21,7 +21,7 @@ describe('datasource/rubygems/index', () => {
 
     const params = {
       versioning: rubyVersioning.id,
-      datasource: rubygems.id,
+      datasource: RubyGemsDatasource.id,
       depName: 'rails',
       registryUrls: [
         'https://thirdparty.com',
diff --git a/lib/datasource/rubygems/index.ts b/lib/datasource/rubygems/index.ts
index 83ca50a1de8f639ae2e3bdd8a3fae9e428667df2..74da9e61e9a88927e44cdd2b81277dfd79d341d5 100644
--- a/lib/datasource/rubygems/index.ts
+++ b/lib/datasource/rubygems/index.ts
@@ -1,8 +1,49 @@
+import { cache } from '../../util/cache/package/decorator';
+import { parseUrl } from '../../util/url';
 import * as rubyVersioning from '../../versioning/ruby';
+import { Datasource } from '../datasource';
+import { GetReleasesConfig, ReleaseResult } from '../types';
+import { InternalRubyGemsDatasource } from './get';
+import { RubyGemsOrgDatasource } from './get-rubygems-org';
 
-export { getReleases } from './releases';
-export { id } from './common';
-export const customRegistrySupport = true;
-export const defaultRegistryUrls = ['https://rubygems.org'];
-export const defaultVersioning = rubyVersioning.id;
-export const registryStrategy = 'hunt';
+export class RubyGemsDatasource extends Datasource {
+  static readonly id = 'rubygems';
+
+  constructor() {
+    super(RubyGemsDatasource.id);
+    this.rubyGemsOrgDatasource = new RubyGemsOrgDatasource(
+      RubyGemsDatasource.id
+    );
+    this.internalRubyGemsDatasource = new InternalRubyGemsDatasource(
+      RubyGemsDatasource.id
+    );
+  }
+
+  override readonly defaultRegistryUrls = ['https://rubygems.org'];
+
+  override readonly defaultVersioning = rubyVersioning.id;
+
+  override readonly registryStrategy = 'hunt';
+
+  private readonly rubyGemsOrgDatasource: RubyGemsOrgDatasource;
+
+  private readonly internalRubyGemsDatasource: InternalRubyGemsDatasource;
+
+  @cache({
+    namespace: `datasource-${RubyGemsDatasource.id}`,
+    key: ({ registryUrl, lookupName }: GetReleasesConfig) =>
+      `${registryUrl}/${lookupName}`,
+  })
+  getReleases({
+    lookupName,
+    registryUrl,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    if (parseUrl(registryUrl)?.hostname === 'rubygems.org') {
+      return this.rubyGemsOrgDatasource.getReleases({ lookupName });
+    }
+    return this.internalRubyGemsDatasource.getReleases({
+      lookupName,
+      registryUrl,
+    });
+  }
+}
diff --git a/lib/datasource/rubygems/releases.ts b/lib/datasource/rubygems/releases.ts
deleted file mode 100644
index 5a4a2c9e9115bced342276218e64c4ae4c624b77..0000000000000000000000000000000000000000
--- a/lib/datasource/rubygems/releases.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { parseUrl } from '../../util/url';
-import type { GetReleasesConfig, ReleaseResult } from '../types';
-import { knownFallbackHosts } from './common';
-import { getDependency, getDependencyFallback } from './get';
-import { getRubygemsOrgDependency } from './get-rubygems-org';
-
-export function getReleases({
-  lookupName,
-  registryUrl,
-}: GetReleasesConfig): Promise<ReleaseResult | null> {
-  if (parseUrl(registryUrl)?.hostname === 'rubygems.org') {
-    return getRubygemsOrgDependency(lookupName);
-  }
-  if (knownFallbackHosts.includes(parseUrl(registryUrl)?.hostname)) {
-    return getDependencyFallback(lookupName, registryUrl);
-  }
-  return getDependency(lookupName, registryUrl);
-}
diff --git a/lib/manager/bundler/extract.ts b/lib/manager/bundler/extract.ts
index 228be6f5f26c3f7fce12c15d0154ad123741c31b..76853e34aed402c1ffbe810c8069b1ff76c18b13 100644
--- a/lib/manager/bundler/extract.ts
+++ b/lib/manager/bundler/extract.ts
@@ -1,4 +1,4 @@
-import * as datasourceRubygems from '../../datasource/rubygems';
+import { RubyGemsDatasource } from '../../datasource/rubygems';
 import { logger } from '../../logger';
 import { SkipReason } from '../../types';
 import { readLocalFile } from '../../util/fs';
@@ -56,7 +56,7 @@ export async function extractPackageFile(
         dep.skipReason = SkipReason.NoVersion;
       }
       if (!dep.skipReason) {
-        dep.datasource = datasourceRubygems.id;
+        dep.datasource = RubyGemsDatasource.id;
       }
       res.deps.push(dep);
     }
diff --git a/lib/workers/repository/init/vulnerability.ts b/lib/workers/repository/init/vulnerability.ts
index 5503daaeead4fb9b8f3dde6eb5279b2138f1d22c..321ddf5bbb5cc90a754599c60a5acdf41d3d7ce8 100644
--- a/lib/workers/repository/init/vulnerability.ts
+++ b/lib/workers/repository/init/vulnerability.ts
@@ -4,7 +4,7 @@ import * as datasourceMaven from '../../../datasource/maven';
 import { id as npmId } from '../../../datasource/npm';
 import * as datasourceNuget from '../../../datasource/nuget';
 import { PypiDatasource } from '../../../datasource/pypi';
-import * as datasourceRubygems from '../../../datasource/rubygems';
+import { RubyGemsDatasource } from '../../../datasource/rubygems';
 import { logger } from '../../../logger';
 import { platform } from '../../../platform';
 import { SecurityAdvisory } from '../../../types';
@@ -91,7 +91,7 @@ export async function detectVulnerabilityAlerts(
         NPM: npmId,
         NUGET: datasourceNuget.id,
         PIP: PypiDatasource.id,
-        RUBYGEMS: datasourceRubygems.id,
+        RUBYGEMS: RubyGemsDatasource.id,
       };
       const datasource =
         datasourceMapping[alert.securityVulnerability.package.ecosystem];