diff --git a/.eslintrc.js b/.eslintrc.js
index c2c970e27d2e4bc56f229f95dae3bebd465ed1ec..8dfbd0dfde506cad674d9d7957e38ecab7f34bd2 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -123,6 +123,8 @@ module.exports = {
         '@typescript-eslint/unbound-method': 0,
 
         'jest/valid-title': [0, { ignoreTypeOfDescribeName: true }],
+        'max-classes-per-file': 0,
+        'class-methods-use-this': 0,
       },
     },
     {
diff --git a/lib/util/cache/package/decorator.spec.ts b/lib/util/cache/package/decorator.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d4ca78a928ee1414ead88cb8d3ab697d8a76ef2d
--- /dev/null
+++ b/lib/util/cache/package/decorator.spec.ts
@@ -0,0 +1,59 @@
+import os from 'os';
+import { mock } from 'jest-mock-extended';
+import { getName } from '../../../../test/util';
+import type { GetReleasesConfig } from '../../../datasource';
+import * as memCache from '../memory';
+import { cache } from './decorator';
+import * as packageCache from '.';
+
+jest.mock('./file');
+
+describe(getName(), () => {
+  const spy = jest.fn(() => Promise.resolve());
+
+  beforeAll(() => {
+    memCache.init();
+    packageCache.init({ cacheDir: os.tmpdir() });
+  });
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  it('should cache string', async () => {
+    class MyClass {
+      @cache({ namespace: 'namespace', key: 'key' })
+      public async getNumber(): Promise<number> {
+        await spy();
+        return Math.random();
+      }
+    }
+    const myClass = new MyClass();
+    expect(await myClass.getNumber()).toEqual(await myClass.getNumber());
+    expect(await myClass.getNumber()).not.toBeUndefined();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+
+  it('should cache function', async () => {
+    class MyClass {
+      @cache({
+        namespace: (arg: GetReleasesConfig) => arg.registryUrl,
+        key: () => 'key',
+      })
+      public async getNumber(_: GetReleasesConfig): Promise<number> {
+        await spy();
+        return Math.random();
+      }
+    }
+    const myClass = new MyClass();
+    const getReleasesConfig: GetReleasesConfig = {
+      registryUrl: 'registry',
+      ...mock<GetReleasesConfig>(),
+    };
+    expect(await myClass.getNumber(getReleasesConfig)).toEqual(
+      await myClass.getNumber(getReleasesConfig)
+    );
+    expect(await myClass.getNumber(getReleasesConfig)).not.toBeUndefined();
+    expect(spy).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/lib/util/cache/package/decorator.ts b/lib/util/cache/package/decorator.ts
new file mode 100644
index 0000000000000000000000000000000000000000..60fc2590c15ab068cece1fddbaa8cf916a2aeb1f
--- /dev/null
+++ b/lib/util/cache/package/decorator.ts
@@ -0,0 +1,126 @@
+import is from '@sindresorhus/is';
+import { logger } from '../../../logger';
+import * as packageCache from '.';
+
+type Handler<T> = (parameters: DecoratorParameters<T>) => Promise<unknown>;
+type Method<T> = (this: T, ...args: any[]) => Promise<any>;
+type Decorator<T> = <U extends T>(
+  target: U,
+  key: keyof U,
+  descriptor: TypedPropertyDescriptor<Method<T>>
+) => TypedPropertyDescriptor<Method<T>>;
+
+interface DecoratorParameters<T, U extends any[] = any[]> {
+  /**
+   * Current call arguments.
+   */
+  args: U;
+
+  /**
+   * A callback to call the decorated method with the current arguments.
+   */
+  callback(): unknown;
+
+  /**
+   * Current call context.
+   */
+  instance: T;
+}
+
+/**
+ * Applies decorating function to intercept decorated method calls.
+ * @param fn - The decorating function.
+ */
+function decorate<T>(fn: Handler<T>): Decorator<T> {
+  const result: Decorator<T> = (
+    target,
+    key,
+    descriptor = Object.getOwnPropertyDescriptor(target, key) ?? {
+      enumerable: true,
+      configurable: true,
+      writable: true,
+    }
+  ) => {
+    const { value } = descriptor;
+
+    return Object.assign(descriptor, {
+      value(this: T, ...args: any[]) {
+        return fn({
+          args,
+          instance: this,
+          callback: () => value?.apply(this, args),
+        });
+      },
+    });
+  };
+
+  return result;
+}
+
+type HashFunction<T extends any[] = any[]> = (...args: T) => string;
+
+/**
+ * The cache decorator parameters.
+ */
+interface CacheParameters {
+  /**
+   * The cache namespace
+   * Either a string or a hash function that generates a string
+   */
+  namespace: string | HashFunction;
+
+  /**
+   * The cache key
+   * Either a string or a hash function that generates a string
+   */
+  key: string | HashFunction;
+
+  /**
+   * The TTL (or expiry) of the key in minutes
+   */
+  ttlMinutes?: number;
+}
+
+/**
+ * caches the result of a decorated method.
+ */
+export function cache<T>({
+  namespace,
+  key,
+  ttlMinutes = 30,
+}: CacheParameters): Decorator<T> {
+  return decorate(async ({ args, instance, callback }) => {
+    try {
+      let finalNamespace: string;
+      if (is.string(namespace)) {
+        finalNamespace = namespace;
+      } else if (is.function_(namespace)) {
+        finalNamespace = namespace.apply(instance, args);
+      }
+
+      let finalKey: string;
+      if (is.string(key)) {
+        finalKey = key;
+      } else if (is.function_(key)) {
+        finalKey = key.apply(instance, args);
+      }
+
+      const cachedResult = await packageCache.get<unknown>(
+        finalNamespace,
+        finalKey
+      );
+
+      if (cachedResult !== undefined) {
+        return cachedResult;
+      }
+
+      const result = await callback();
+
+      await packageCache.set(finalNamespace, finalKey, result, ttlMinutes);
+      return result;
+    } catch (err) /* istanbul ignore next */ {
+      logger.error(err);
+      throw err;
+    }
+  });
+}
diff --git a/tsconfig.json b/tsconfig.json
index 0099ae0294bbe26c9fcc0d4bba5e69bbd1bfb070..f3cee72242a32ac3c27b08460c8d6e7e7a0641b1 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,7 @@
     "esModuleInterop": true,
     "resolveJsonModule": false,
     "noUnusedLocals": true,
+    "experimentalDecorators": true,
     "lib": ["es2018"],
     "types": ["node", "jest", "jest-extended"],
     "allowJs": true,