diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts
index 6d78052..4202946 100644
--- a/__tests__/restore.test.ts
+++ b/__tests__/restore.test.ts
@@ -1,7 +1,7 @@
 import * as cache from "@actions/cache";
 import * as core from "@actions/core";
 
-import { Events, Inputs, RefKey } from "../src/constants";
+import { Events, RefKey } from "../src/constants";
 import run from "../src/restore";
 import * as actionUtils from "../src/utils/actionUtils";
 import * as testUtils from "../src/utils/testUtils";
@@ -45,158 +45,6 @@ afterEach(() => {
     delete process.env[RefKey];
 });
 
-test("restore with invalid event outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const invalidEvent = "commit_comment";
-    process.env[Events.Key] = invalidEvent;
-    delete process.env[RefKey];
-    await run();
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("restore without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
-});
-
-test("restore on GHES without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
-});
-
-test("restore on GHES with AC available ", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    const path = "node_modules";
-    const key = "node-test";
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
-    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
-    const restoreCacheMock = jest
-        .spyOn(cache, "restoreCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(key);
-        });
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
-
-    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("restore with no path should fail", async () => {
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    // this input isn't necessary for restore b/c tarball contains entries relative to workspace
-    expect(failedMock).not.toHaveBeenCalledWith(
-        "Input required and not supplied: path"
-    );
-});
-
-test("restore with no key", async () => {
-    testUtils.setInput(Inputs.Path, "node_modules");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(failedMock).toHaveBeenCalledWith(
-        "Input required and not supplied: key"
-    );
-});
-
-test("restore with too many keys should fail", async () => {
-    const path = "node_modules";
-    const key = "node-test";
-    const restoreKeys = [...Array(20).keys()].map(x => x.toString());
-    testUtils.setInputs({
-        path: path,
-        key,
-        restoreKeys
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: Keys are limited to a maximum of 10.`
-    );
-});
-
-test("restore with large key should fail", async () => {
-    const path = "node_modules";
-    const key = "foo".repeat(512); // Over the 512 character limit
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot be larger than 512 characters.`
-    );
-});
-
-test("restore with invalid key should fail", async () => {
-    const path = "node_modules";
-    const key = "comma,comma";
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot contain commas.`
-    );
-});
-
 test("restore with no cache found", async () => {
     const path = "node_modules";
     const key = "node-test";
diff --git a/__tests__/restoreImpl.test.ts b/__tests__/restoreImpl.test.ts
new file mode 100644
index 0000000..66bab8d
--- /dev/null
+++ b/__tests__/restoreImpl.test.ts
@@ -0,0 +1,326 @@
+import * as cache from "@actions/cache";
+import * as core from "@actions/core";
+
+import { Events, Inputs, RefKey } from "../src/constants";
+import run from "../src/restoreImpl";
+import { StateProvider } from "../src/stateProvider";
+import * as actionUtils from "../src/utils/actionUtils";
+import * as testUtils from "../src/utils/testUtils";
+
+jest.mock("../src/utils/actionUtils");
+
+beforeAll(() => {
+    jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
+        (key, cacheResult) => {
+            const actualUtils = jest.requireActual("../src/utils/actionUtils");
+            return actualUtils.isExactKeyMatch(key, cacheResult);
+        }
+    );
+
+    jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
+        const actualUtils = jest.requireActual("../src/utils/actionUtils");
+        return actualUtils.isValidEvent();
+    });
+
+    jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
+        (name, options) => {
+            const actualUtils = jest.requireActual("../src/utils/actionUtils");
+            return actualUtils.getInputAsArray(name, options);
+        }
+    );
+});
+
+beforeEach(() => {
+    process.env[Events.Key] = Events.Push;
+    process.env[RefKey] = "refs/heads/feature-branch";
+
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => true
+    );
+});
+
+afterEach(() => {
+    testUtils.clearInputs();
+    delete process.env[Events.Key];
+    delete process.env[RefKey];
+});
+
+test("restore with invalid event outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const invalidEvent = "commit_comment";
+    process.env[Events.Key] = invalidEvent;
+    delete process.env[RefKey];
+    await run(new StateProvider());
+    expect(logWarningMock).toHaveBeenCalledWith(
+        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
+    );
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("restore without AC available should no-op", async () => {
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => false
+    );
+
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+});
+
+test("restore on GHES without AC available should no-op", async () => {
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => false
+    );
+
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+});
+
+test("restore on GHES with AC available ", async () => {
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
+    const path = "node_modules";
+    const key = "node-test";
+    testUtils.setInputs({
+        path: path,
+        key
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(key);
+        });
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
+
+    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("restore with no path should fail", async () => {
+    const failedMock = jest.spyOn(core, "setFailed");
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    await run(new StateProvider());
+    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
+    // this input isn't necessary for restore b/c tarball contains entries relative to workspace
+    expect(failedMock).not.toHaveBeenCalledWith(
+        "Input required and not supplied: path"
+    );
+});
+
+test("restore with no key", async () => {
+    testUtils.setInput(Inputs.Path, "node_modules");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    await run(new StateProvider());
+    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
+    expect(failedMock).toHaveBeenCalledWith(
+        "Input required and not supplied: key"
+    );
+});
+
+test("restore with too many keys should fail", async () => {
+    const path = "node_modules";
+    const key = "node-test";
+    const restoreKeys = [...Array(20).keys()].map(x => x.toString());
+    testUtils.setInputs({
+        path: path,
+        key,
+        restoreKeys
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    await run(new StateProvider());
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: Keys are limited to a maximum of 10.`
+    );
+});
+
+test("restore with large key should fail", async () => {
+    const path = "node_modules";
+    const key = "foo".repeat(512); // Over the 512 character limit
+    testUtils.setInputs({
+        path: path,
+        key
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    await run(new StateProvider());
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: ${key} cannot be larger than 512 characters.`
+    );
+});
+
+test("restore with invalid key should fail", async () => {
+    const path = "node_modules";
+    const key = "comma,comma";
+    testUtils.setInputs({
+        path: path,
+        key
+    });
+    const failedMock = jest.spyOn(core, "setFailed");
+    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
+    await run(new StateProvider());
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
+    expect(failedMock).toHaveBeenCalledWith(
+        `Key Validation Error: ${key} cannot contain commas.`
+    );
+});
+
+test("restore with no cache found", async () => {
+    const path = "node_modules";
+    const key = "node-test";
+    testUtils.setInputs({
+        path: path,
+        key
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(undefined);
+        });
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache not found for input keys: ${key}`
+    );
+});
+
+test("restore with restore keys and no cache found", async () => {
+    const path = "node_modules";
+    const key = "node-test";
+    const restoreKey = "node-";
+    testUtils.setInputs({
+        path: path,
+        key,
+        restoreKeys: [restoreKey]
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(undefined);
+        });
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache not found for input keys: ${key}, ${restoreKey}`
+    );
+});
+
+test("restore with cache found for key", async () => {
+    const path = "node_modules";
+    const key = "node-test";
+    testUtils.setInputs({
+        path: path,
+        key
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(key);
+        });
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
+
+    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("restore with cache found for restore key", async () => {
+    const path = "node_modules";
+    const key = "node-test";
+    const restoreKey = "node-";
+    testUtils.setInputs({
+        path: path,
+        key,
+        restoreKeys: [restoreKey]
+    });
+
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const stateMock = jest.spyOn(core, "saveState");
+    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(restoreKey);
+        });
+
+    await run(new StateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
+
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache restored from key: ${restoreKey}`
+    );
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
diff --git a/__tests__/restoreOnly.test.ts b/__tests__/restoreOnly.test.ts
index ee9779c..4812f02 100644
--- a/__tests__/restoreOnly.test.ts
+++ b/__tests__/restoreOnly.test.ts
@@ -1,8 +1,9 @@
 import * as cache from "@actions/cache";
 import * as core from "@actions/core";
 
-import { Events, Inputs, RefKey } from "../src/constants";
-import run from "../src/restoreOnly";
+import { Events, RefKey } from "../src/constants";
+import run from "../src/restoreImpl";
+import { StateProvider } from "../src/stateProvider";
 import * as actionUtils from "../src/utils/actionUtils";
 import * as testUtils from "../src/utils/testUtils";
 
@@ -45,157 +46,6 @@ afterEach(() => {
     delete process.env[RefKey];
 });
 
-test("restore with invalid event outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const invalidEvent = "commit_comment";
-    process.env[Events.Key] = invalidEvent;
-    delete process.env[RefKey];
-    await run();
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("restore without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    const outputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(outputMock).toHaveBeenCalledTimes(1);
-    expect(outputMock).toHaveBeenCalledWith(false);
-});
-
-test("restore on GHES without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    const outputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(outputMock).toHaveBeenCalledTimes(1);
-    expect(outputMock).toHaveBeenCalledWith(false);
-});
-
-test("restore on GHES with AC available ", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    const path = "node_modules";
-    const key = "node-test";
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const outputMock = jest.spyOn(core, "setOutput");
-    const restoreCacheMock = jest
-        .spyOn(cache, "restoreCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(key);
-        });
-
-    await run();
-
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-
-    expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(outputMock).toHaveBeenCalledTimes(3);
-    expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
-
-    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("restore with no path should fail", async () => {
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    // this input isn't necessary for restore b/c tarball contains entries relative to workspace
-    expect(failedMock).not.toHaveBeenCalledWith(
-        "Input required and not supplied: path"
-    );
-});
-
-test("restore with no key", async () => {
-    testUtils.setInput(Inputs.Path, "node_modules");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(0);
-    expect(failedMock).toHaveBeenCalledWith(
-        "Input required and not supplied: key"
-    );
-});
-
-test("restore with too many keys should fail", async () => {
-    const path = "node_modules";
-    const key = "node-test";
-    const restoreKeys = [...Array(20).keys()].map(x => x.toString());
-    testUtils.setInputs({
-        path: path,
-        key,
-        restoreKeys
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: Keys are limited to a maximum of 10.`
-    );
-});
-
-test("restore with large key should fail", async () => {
-    const path = "node_modules";
-    const key = "foo".repeat(512); // Over the 512 character limit
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot be larger than 512 characters.`
-    );
-});
-
-test("restore with invalid key should fail", async () => {
-    const path = "node_modules";
-    const key = "comma,comma";
-    testUtils.setInputs({
-        path: path,
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    const restoreCacheMock = jest.spyOn(cache, "restoreCache");
-    await run();
-    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
-    expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot contain commas.`
-    );
-});
-
 test("restore with no cache found", async () => {
     const path = "node_modules";
     const key = "node-test";
@@ -206,19 +56,19 @@ test("restore with no cache found", async () => {
 
     const infoMock = jest.spyOn(core, "info");
     const failedMock = jest.spyOn(core, "setFailed");
-    const outputMock = jest.spyOn(core, "setOutput");
+    const stateMock = jest.spyOn(core, "saveState");
     const restoreCacheMock = jest
         .spyOn(cache, "restoreCache")
         .mockImplementationOnce(() => {
             return Promise.resolve(undefined);
         });
 
-    await run();
+    await run(new StateProvider());
 
     expect(restoreCacheMock).toHaveBeenCalledTimes(1);
     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
 
-    expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
     expect(failedMock).toHaveBeenCalledTimes(0);
 
     expect(infoMock).toHaveBeenCalledWith(
@@ -238,19 +88,19 @@ test("restore with restore keys and no cache found", async () => {
 
     const infoMock = jest.spyOn(core, "info");
     const failedMock = jest.spyOn(core, "setFailed");
-    const outputMock = jest.spyOn(core, "setOutput");
+    const stateMock = jest.spyOn(core, "saveState");
     const restoreCacheMock = jest
         .spyOn(cache, "restoreCache")
         .mockImplementationOnce(() => {
             return Promise.resolve(undefined);
         });
 
-    await run();
+    await run(new StateProvider());
 
     expect(restoreCacheMock).toHaveBeenCalledTimes(1);
     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
 
-    expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
     expect(failedMock).toHaveBeenCalledTimes(0);
 
     expect(infoMock).toHaveBeenCalledWith(
@@ -268,21 +118,22 @@ test("restore with cache found for key", async () => {
 
     const infoMock = jest.spyOn(core, "info");
     const failedMock = jest.spyOn(core, "setFailed");
-    const outputMock = jest.spyOn(core, "setOutput");
+    const stateMock = jest.spyOn(core, "saveState");
+    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
     const restoreCacheMock = jest
         .spyOn(cache, "restoreCache")
         .mockImplementationOnce(() => {
             return Promise.resolve(key);
         });
 
-    await run();
+    await run(new StateProvider());
 
     expect(restoreCacheMock).toHaveBeenCalledTimes(1);
     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
 
-    expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(outputMock).toHaveBeenCalledTimes(3);
-    expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
 
     expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
     expect(failedMock).toHaveBeenCalledTimes(0);
@@ -300,21 +151,22 @@ test("restore with cache found for restore key", async () => {
 
     const infoMock = jest.spyOn(core, "info");
     const failedMock = jest.spyOn(core, "setFailed");
-    const outputMock = jest.spyOn(core, "setOutput");
+    const stateMock = jest.spyOn(core, "saveState");
+    const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
     const restoreCacheMock = jest
         .spyOn(cache, "restoreCache")
         .mockImplementationOnce(() => {
             return Promise.resolve(restoreKey);
         });
 
-    await run();
+    await run(new StateProvider());
 
     expect(restoreCacheMock).toHaveBeenCalledTimes(1);
     expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
 
-    expect(outputMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(outputMock).toHaveBeenCalledTimes(3);
-    expect(outputMock).toHaveBeenCalledWith("cache-hit", "false");
+    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+    expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
     expect(infoMock).toHaveBeenCalledWith(
         `Cache restored from key: ${restoreKey}`
     );
diff --git a/__tests__/save.test.ts b/__tests__/save.test.ts
index 3c550e0..8b3b356 100644
--- a/__tests__/save.test.ts
+++ b/__tests__/save.test.ts
@@ -61,294 +61,6 @@ afterEach(() => {
     delete process.env[RefKey];
 });
 
-test("save with invalid event outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const invalidEvent = "commit_comment";
-    process.env[Events.Key] = invalidEvent;
-    delete process.env[RefKey];
-    await run();
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with no primary key in state outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return "";
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Error retrieving key from state.`
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-});
-
-test("save on ghes without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-});
-
-test("save on GHES with AC available", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-    testUtils.setInput(Inputs.UploadChunkSize, "4000000");
-
-    const cacheId = 4;
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(cacheId);
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
-        uploadChunkSize: 4000000
-    });
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with exact match returns early", async () => {
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = primaryKey;
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(infoMock).toHaveBeenCalledWith(
-        `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with missing input outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        "Input required and not supplied: path"
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with large cache outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            throw new Error(
-                "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
-            );
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with reserve cache failure outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            const actualCache = jest.requireActual("@actions/cache");
-            const error = new actualCache.ReserveCacheError(
-                `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
-            );
-            throw error;
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with server error outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            throw new Error("HTTP Error Occurred");
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
 test("save with valid inputs uploads a cache", async () => {
     const failedMock = jest.spyOn(core, "setFailed");
 
diff --git a/__tests__/saveImpl.test.ts b/__tests__/saveImpl.test.ts
new file mode 100644
index 0000000..78e964a
--- /dev/null
+++ b/__tests__/saveImpl.test.ts
@@ -0,0 +1,388 @@
+import * as cache from "@actions/cache";
+import * as core from "@actions/core";
+
+import { Events, Inputs, RefKey } from "../src/constants";
+import run from "../src/saveImpl";
+import { StateProvider } from "../src/stateProvider";
+import * as actionUtils from "../src/utils/actionUtils";
+import * as testUtils from "../src/utils/testUtils";
+
+jest.mock("@actions/core");
+jest.mock("@actions/cache");
+jest.mock("../src/utils/actionUtils");
+
+beforeAll(() => {
+    jest.spyOn(core, "getInput").mockImplementation((name, options) => {
+        return jest.requireActual("@actions/core").getInput(name, options);
+    });
+
+    jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
+        (name, options) => {
+            return jest
+                .requireActual("../src/utils/actionUtils")
+                .getInputAsArray(name, options);
+        }
+    );
+
+    jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
+        (name, options) => {
+            return jest
+                .requireActual("../src/utils/actionUtils")
+                .getInputAsInt(name, options);
+        }
+    );
+
+    jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
+        (key, cacheResult) => {
+            return jest
+                .requireActual("../src/utils/actionUtils")
+                .isExactKeyMatch(key, cacheResult);
+        }
+    );
+
+    jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
+        const actualUtils = jest.requireActual("../src/utils/actionUtils");
+        return actualUtils.isValidEvent();
+    });
+});
+
+beforeEach(() => {
+    process.env[Events.Key] = Events.Push;
+    process.env[RefKey] = "refs/heads/feature-branch";
+
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => true
+    );
+});
+
+afterEach(() => {
+    testUtils.clearInputs();
+    delete process.env[Events.Key];
+    delete process.env[RefKey];
+});
+
+test("save with invalid event outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+    const invalidEvent = "commit_comment";
+    process.env[Events.Key] = invalidEvent;
+    delete process.env[RefKey];
+    await run(new StateProvider());
+    expect(logWarningMock).toHaveBeenCalledWith(
+        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
+    );
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with no primary key in state outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return "";
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        });
+    const saveCacheMock = jest.spyOn(cache, "saveCache");
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(0);
+    expect(logWarningMock).toHaveBeenCalledWith(
+        `Error retrieving key from state.`
+    );
+    expect(logWarningMock).toHaveBeenCalledTimes(1);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save without AC available should no-op", async () => {
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => false
+    );
+
+    const saveCacheMock = jest.spyOn(cache, "saveCache");
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(0);
+});
+
+test("save on ghes without AC available should no-op", async () => {
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
+    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
+        () => false
+    );
+
+    const saveCacheMock = jest.spyOn(cache, "saveCache");
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(0);
+});
+
+test("save on GHES with AC available", async () => {
+    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Path, inputPath);
+    testUtils.setInput(Inputs.UploadChunkSize, "4000000");
+
+    const cacheId = 4;
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(cacheId);
+        });
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
+        uploadChunkSize: 4000000
+    });
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with exact match returns early", async () => {
+    const infoMock = jest.spyOn(core, "info");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = primaryKey;
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+    const saveCacheMock = jest.spyOn(cache, "saveCache");
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(0);
+    expect(infoMock).toHaveBeenCalledWith(
+        `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
+    );
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with missing input outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+    const saveCacheMock = jest.spyOn(cache, "saveCache");
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(0);
+    expect(logWarningMock).toHaveBeenCalledWith(
+        "Input required and not supplied: path"
+    );
+    expect(logWarningMock).toHaveBeenCalledTimes(1);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with large cache outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Path, inputPath);
+
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            throw new Error(
+                "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
+            );
+        });
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        expect.anything()
+    );
+
+    expect(logWarningMock).toHaveBeenCalledTimes(1);
+    expect(logWarningMock).toHaveBeenCalledWith(
+        "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
+    );
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with reserve cache failure outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Path, inputPath);
+
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            const actualCache = jest.requireActual("@actions/cache");
+            const error = new actualCache.ReserveCacheError(
+                `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
+            );
+            throw error;
+        });
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        expect.anything()
+    );
+
+    expect(logWarningMock).toHaveBeenCalledWith(
+        `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
+    );
+    expect(logWarningMock).toHaveBeenCalledTimes(1);
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with server error outputs warning", async () => {
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Path, inputPath);
+
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            throw new Error("HTTP Error Occurred");
+        });
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        expect.anything()
+    );
+
+    expect(logWarningMock).toHaveBeenCalledTimes(1);
+    expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("save with valid inputs uploads a cache", async () => {
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+    const savedCacheKey = "Linux-node-";
+
+    jest.spyOn(core, "getState")
+        // Cache Entry State
+        .mockImplementationOnce(() => {
+            return savedCacheKey;
+        })
+        // Cache Key State
+        .mockImplementationOnce(() => {
+            return primaryKey;
+        });
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Path, inputPath);
+    testUtils.setInput(Inputs.UploadChunkSize, "4000000");
+
+    const cacheId = 4;
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(cacheId);
+        });
+
+    await run(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
+        uploadChunkSize: 4000000
+    });
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
diff --git a/__tests__/saveOnly.test.ts b/__tests__/saveOnly.test.ts
index 3c550e0..5604e3a 100644
--- a/__tests__/saveOnly.test.ts
+++ b/__tests__/saveOnly.test.ts
@@ -2,7 +2,7 @@ import * as cache from "@actions/cache";
 import * as core from "@actions/core";
 
 import { Events, Inputs, RefKey } from "../src/constants";
-import run from "../src/save";
+import run from "../src/saveOnly";
 import * as actionUtils from "../src/utils/actionUtils";
 import * as testUtils from "../src/utils/testUtils";
 
@@ -61,311 +61,13 @@ afterEach(() => {
     delete process.env[RefKey];
 });
 
-test("save with invalid event outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const invalidEvent = "commit_comment";
-    process.env[Events.Key] = invalidEvent;
-    delete process.env[RefKey];
-    await run();
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with no primary key in state outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return "";
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Error retrieving key from state.`
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-});
-
-test("save on ghes without AC available should no-op", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
-        () => false
-    );
-
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-});
-
-test("save on GHES with AC available", async () => {
-    jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-    testUtils.setInput(Inputs.UploadChunkSize, "4000000");
-
-    const cacheId = 4;
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(cacheId);
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
-        uploadChunkSize: 4000000
-    });
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with exact match returns early", async () => {
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = primaryKey;
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(infoMock).toHaveBeenCalledWith(
-        `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with missing input outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-    const saveCacheMock = jest.spyOn(cache, "saveCache");
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        "Input required and not supplied: path"
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with large cache outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            throw new Error(
-                "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
-            );
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith(
-        "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with reserve cache failure outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            const actualCache = jest.requireActual("@actions/cache");
-            const error = new actualCache.ReserveCacheError(
-                `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
-            );
-            throw error;
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
-test("save with server error outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const saveCacheMock = jest
-        .spyOn(cache, "saveCache")
-        .mockImplementationOnce(() => {
-            throw new Error("HTTP Error Occurred");
-        });
-
-    await run();
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(
-        [inputPath],
-        primaryKey,
-        expect.anything()
-    );
-
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
-
 test("save with valid inputs uploads a cache", async () => {
     const failedMock = jest.spyOn(core, "setFailed");
 
     const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const savedCacheKey = "Linux-node-";
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return savedCacheKey;
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
 
     const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Key, primaryKey);
     testUtils.setInput(Inputs.Path, inputPath);
     testUtils.setInput(Inputs.UploadChunkSize, "4000000");
 
diff --git a/dist/restore-only/index.js b/dist/restore-only/index.js
index b646cbf..961c8a6 100644
--- a/dist/restore-only/index.js
+++ b/dist/restore-only/index.js
@@ -4943,20 +4943,19 @@ exports.checkBypass = checkBypass;
 "use strict";
 
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
+exports.stateToOutputMap = exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
 var Inputs;
 (function (Inputs) {
     Inputs["Key"] = "key";
     Inputs["Path"] = "path";
     Inputs["RestoreKeys"] = "restore-keys";
-    Inputs["UploadChunkSize"] = "upload-chunk-size";
-    Inputs["RestoredKey"] = "restored-key"; // Input from save action
+    Inputs["UploadChunkSize"] = "upload-chunk-size"; // Input for cache, save action
 })(Inputs = exports.Inputs || (exports.Inputs = {}));
 var Outputs;
 (function (Outputs) {
     Outputs["CacheHit"] = "cache-hit";
-    Outputs["InputtedKey"] = "inputted-key";
-    Outputs["MatchedKey"] = "matched-key"; // Output from restore action
+    Outputs["CachePrimaryKey"] = "cache-primary-key";
+    Outputs["CacheRestoreKey"] = "cache-restore-key"; // Output from restore action
 })(Outputs = exports.Outputs || (exports.Outputs = {}));
 var State;
 (function (State) {
@@ -4970,6 +4969,10 @@ var Events;
     Events["PullRequest"] = "pull_request";
 })(Events = exports.Events || (exports.Events = {}));
 exports.RefKey = "GITHUB_REF";
+exports.stateToOutputMap = new Map([
+    [State.CacheMatchedKey, Outputs.CacheRestoreKey],
+    [State.CachePrimaryKey, Outputs.CachePrimaryKey]
+]);
 
 
 /***/ }),
@@ -9361,7 +9364,7 @@ const constants_1 = __webpack_require__(196);
 class StateProviderBase {
     constructor() {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
-        this.setState = (key, value, outputKey) => { };
+        this.setState = (key, value) => { };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
     }
@@ -9385,10 +9388,8 @@ exports.StateProvider = StateProvider;
 class NullStateProvider extends StateProviderBase {
     constructor() {
         super(...arguments);
-        this.setState = (key, value, outputKey) => {
-            if (outputKey) {
-                core.setOutput(outputKey, value);
-            }
+        this.setState = (key, value) => {
+            core.setOutput(constants_1.stateToOutputMap.get(key), value);
         };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
@@ -50467,7 +50468,7 @@ function restoreImpl(stateProvider) {
                 return;
             }
             const primaryKey = core.getInput(constants_1.Inputs.Key, { required: true });
-            stateProvider.setState(constants_1.State.CachePrimaryKey, primaryKey, constants_1.Outputs.InputtedKey);
+            stateProvider.setState(constants_1.State.CachePrimaryKey, primaryKey);
             const restoreKeys = utils.getInputAsArray(constants_1.Inputs.RestoreKeys);
             const cachePaths = utils.getInputAsArray(constants_1.Inputs.Path, {
                 required: true
@@ -50481,7 +50482,7 @@ function restoreImpl(stateProvider) {
                 return;
             }
             // Store the matched cache key in states
-            stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey, constants_1.Outputs.MatchedKey);
+            stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey);
             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey);
             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString());
             core.info(`Cache restored from key: ${cacheKey}`);
diff --git a/dist/restore/index.js b/dist/restore/index.js
index 18e2e92..af749dc 100644
--- a/dist/restore/index.js
+++ b/dist/restore/index.js
@@ -4943,20 +4943,19 @@ exports.checkBypass = checkBypass;
 "use strict";
 
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
+exports.stateToOutputMap = exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
 var Inputs;
 (function (Inputs) {
     Inputs["Key"] = "key";
     Inputs["Path"] = "path";
     Inputs["RestoreKeys"] = "restore-keys";
-    Inputs["UploadChunkSize"] = "upload-chunk-size";
-    Inputs["RestoredKey"] = "restored-key"; // Input from save action
+    Inputs["UploadChunkSize"] = "upload-chunk-size"; // Input for cache, save action
 })(Inputs = exports.Inputs || (exports.Inputs = {}));
 var Outputs;
 (function (Outputs) {
     Outputs["CacheHit"] = "cache-hit";
-    Outputs["InputtedKey"] = "inputted-key";
-    Outputs["MatchedKey"] = "matched-key"; // Output from restore action
+    Outputs["CachePrimaryKey"] = "cache-primary-key";
+    Outputs["CacheRestoreKey"] = "cache-restore-key"; // Output from restore action
 })(Outputs = exports.Outputs || (exports.Outputs = {}));
 var State;
 (function (State) {
@@ -4970,6 +4969,10 @@ var Events;
     Events["PullRequest"] = "pull_request";
 })(Events = exports.Events || (exports.Events = {}));
 exports.RefKey = "GITHUB_REF";
+exports.stateToOutputMap = new Map([
+    [State.CacheMatchedKey, Outputs.CacheRestoreKey],
+    [State.CachePrimaryKey, Outputs.CachePrimaryKey]
+]);
 
 
 /***/ }),
@@ -9361,7 +9364,7 @@ const constants_1 = __webpack_require__(196);
 class StateProviderBase {
     constructor() {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
-        this.setState = (key, value, outputKey) => { };
+        this.setState = (key, value) => { };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
     }
@@ -9385,10 +9388,8 @@ exports.StateProvider = StateProvider;
 class NullStateProvider extends StateProviderBase {
     constructor() {
         super(...arguments);
-        this.setState = (key, value, outputKey) => {
-            if (outputKey) {
-                core.setOutput(outputKey, value);
-            }
+        this.setState = (key, value) => {
+            core.setOutput(constants_1.stateToOutputMap.get(key), value);
         };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
@@ -50467,7 +50468,7 @@ function restoreImpl(stateProvider) {
                 return;
             }
             const primaryKey = core.getInput(constants_1.Inputs.Key, { required: true });
-            stateProvider.setState(constants_1.State.CachePrimaryKey, primaryKey, constants_1.Outputs.InputtedKey);
+            stateProvider.setState(constants_1.State.CachePrimaryKey, primaryKey);
             const restoreKeys = utils.getInputAsArray(constants_1.Inputs.RestoreKeys);
             const cachePaths = utils.getInputAsArray(constants_1.Inputs.Path, {
                 required: true
@@ -50481,7 +50482,7 @@ function restoreImpl(stateProvider) {
                 return;
             }
             // Store the matched cache key in states
-            stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey, constants_1.Outputs.MatchedKey);
+            stateProvider.setState(constants_1.State.CacheMatchedKey, cacheKey);
             const isExactKeyMatch = utils.isExactKeyMatch(core.getInput(constants_1.Inputs.Key, { required: true }), cacheKey);
             core.setOutput(constants_1.Outputs.CacheHit, isExactKeyMatch.toString());
             core.info(`Cache restored from key: ${cacheKey}`);
diff --git a/dist/save-only/index.js b/dist/save-only/index.js
index 9c29969..96dd341 100644
--- a/dist/save-only/index.js
+++ b/dist/save-only/index.js
@@ -4972,20 +4972,19 @@ exports.checkBypass = checkBypass;
 "use strict";
 
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
+exports.stateToOutputMap = exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
 var Inputs;
 (function (Inputs) {
     Inputs["Key"] = "key";
     Inputs["Path"] = "path";
     Inputs["RestoreKeys"] = "restore-keys";
-    Inputs["UploadChunkSize"] = "upload-chunk-size";
-    Inputs["RestoredKey"] = "restored-key"; // Input from save action
+    Inputs["UploadChunkSize"] = "upload-chunk-size"; // Input for cache, save action
 })(Inputs = exports.Inputs || (exports.Inputs = {}));
 var Outputs;
 (function (Outputs) {
     Outputs["CacheHit"] = "cache-hit";
-    Outputs["InputtedKey"] = "inputted-key";
-    Outputs["MatchedKey"] = "matched-key"; // Output from restore action
+    Outputs["CachePrimaryKey"] = "cache-primary-key";
+    Outputs["CacheRestoreKey"] = "cache-restore-key"; // Output from restore action
 })(Outputs = exports.Outputs || (exports.Outputs = {}));
 var State;
 (function (State) {
@@ -4999,6 +4998,10 @@ var Events;
     Events["PullRequest"] = "pull_request";
 })(Events = exports.Events || (exports.Events = {}));
 exports.RefKey = "GITHUB_REF";
+exports.stateToOutputMap = new Map([
+    [State.CacheMatchedKey, Outputs.CacheRestoreKey],
+    [State.CachePrimaryKey, Outputs.CachePrimaryKey]
+]);
 
 
 /***/ }),
@@ -9390,7 +9393,7 @@ const constants_1 = __webpack_require__(196);
 class StateProviderBase {
     constructor() {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
-        this.setState = (key, value, outputKey) => { };
+        this.setState = (key, value) => { };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
     }
@@ -9414,10 +9417,8 @@ exports.StateProvider = StateProvider;
 class NullStateProvider extends StateProviderBase {
     constructor() {
         super(...arguments);
-        this.setState = (key, value, outputKey) => {
-            if (outputKey) {
-                core.setOutput(outputKey, value);
-            }
+        this.setState = (key, value) => {
+            core.setOutput(constants_1.stateToOutputMap.get(key), value);
         };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
@@ -41111,7 +41112,7 @@ function saveImpl(stateProvider) {
             }
             // If matched restore key is same as primary key, then do not save cache
             // NO-OP in case of SaveOnly action
-            const restoredKey = stateProvider.getCacheState() || core.getInput(constants_1.Inputs.RestoredKey);
+            const restoredKey = stateProvider.getCacheState();
             if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
                 core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
                 return;
diff --git a/dist/save/index.js b/dist/save/index.js
index 69ea907..3032065 100644
--- a/dist/save/index.js
+++ b/dist/save/index.js
@@ -4943,20 +4943,19 @@ exports.checkBypass = checkBypass;
 "use strict";
 
 Object.defineProperty(exports, "__esModule", { value: true });
-exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
+exports.stateToOutputMap = exports.RefKey = exports.Events = exports.State = exports.Outputs = exports.Inputs = void 0;
 var Inputs;
 (function (Inputs) {
     Inputs["Key"] = "key";
     Inputs["Path"] = "path";
     Inputs["RestoreKeys"] = "restore-keys";
-    Inputs["UploadChunkSize"] = "upload-chunk-size";
-    Inputs["RestoredKey"] = "restored-key"; // Input from save action
+    Inputs["UploadChunkSize"] = "upload-chunk-size"; // Input for cache, save action
 })(Inputs = exports.Inputs || (exports.Inputs = {}));
 var Outputs;
 (function (Outputs) {
     Outputs["CacheHit"] = "cache-hit";
-    Outputs["InputtedKey"] = "inputted-key";
-    Outputs["MatchedKey"] = "matched-key"; // Output from restore action
+    Outputs["CachePrimaryKey"] = "cache-primary-key";
+    Outputs["CacheRestoreKey"] = "cache-restore-key"; // Output from restore action
 })(Outputs = exports.Outputs || (exports.Outputs = {}));
 var State;
 (function (State) {
@@ -4970,6 +4969,10 @@ var Events;
     Events["PullRequest"] = "pull_request";
 })(Events = exports.Events || (exports.Events = {}));
 exports.RefKey = "GITHUB_REF";
+exports.stateToOutputMap = new Map([
+    [State.CacheMatchedKey, Outputs.CacheRestoreKey],
+    [State.CachePrimaryKey, Outputs.CachePrimaryKey]
+]);
 
 
 /***/ }),
@@ -9361,7 +9364,7 @@ const constants_1 = __webpack_require__(196);
 class StateProviderBase {
     constructor() {
         // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
-        this.setState = (key, value, outputKey) => { };
+        this.setState = (key, value) => { };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
     }
@@ -9385,10 +9388,8 @@ exports.StateProvider = StateProvider;
 class NullStateProvider extends StateProviderBase {
     constructor() {
         super(...arguments);
-        this.setState = (key, value, outputKey) => {
-            if (outputKey) {
-                core.setOutput(outputKey, value);
-            }
+        this.setState = (key, value) => {
+            core.setOutput(constants_1.stateToOutputMap.get(key), value);
         };
         // eslint-disable-next-line @typescript-eslint/no-unused-vars
         this.getState = (key) => "";
@@ -41082,7 +41083,7 @@ function saveImpl(stateProvider) {
             }
             // If matched restore key is same as primary key, then do not save cache
             // NO-OP in case of SaveOnly action
-            const restoredKey = stateProvider.getCacheState() || core.getInput(constants_1.Inputs.RestoredKey);
+            const restoredKey = stateProvider.getCacheState();
             if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
                 core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
                 return;
diff --git a/restore/action.yml b/restore/action.yml
index 4729458..ef5681e 100644
--- a/restore/action.yml
+++ b/restore/action.yml
@@ -14,9 +14,9 @@ inputs:
 outputs:
   cache-hit:
     description: 'A boolean value to indicate an exact match was found for the primary key'
-  inputted-key:
-    description: 'Key passed in the input to use in subsequent steps of the workflow'
-  matched-key:
+  cache-primary-key:
+    description: 'Cache primary key passed in the input to use in subsequent steps of the workflow'
+  cache-restore-key:
     description: 'Cache key restored'
 runs:
   using: 'node16'
diff --git a/save/action.yml b/save/action.yml
index 220de82..85414eb 100644
--- a/save/action.yml
+++ b/save/action.yml
@@ -11,9 +11,6 @@ inputs:
   upload-chunk-size:
     description: 'The chunk size used to split up large files during upload, in bytes'
     required: false
-  restored-key:
-    description: 'Cache key restored from the restore action'
-    required: false
 runs:
   using: 'node16'
   main: '../dist/save-only/index.js'
diff --git a/src/constants.ts b/src/constants.ts
index 69d6379..091b2a1 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,15 +1,14 @@
 export enum Inputs {
-    Key = "key", // Input from cache, restore, save action
-    Path = "path", // Input from cache, restore, save action
-    RestoreKeys = "restore-keys", // Input from cache, restore action
-    UploadChunkSize = "upload-chunk-size", // Input from cache, save action
-    RestoredKey = "restored-key" // Input from save action
+    Key = "key", // Input for cache, restore, save action
+    Path = "path", // Input for cache, restore, save action
+    RestoreKeys = "restore-keys", // Input for cache, restore action
+    UploadChunkSize = "upload-chunk-size" // Input for cache, save action
 }
 
 export enum Outputs {
     CacheHit = "cache-hit", // Output from cache, restore action
-    InputtedKey = "inputted-key", // Output from restore action
-    MatchedKey = "matched-key" // Output from restore action
+    CachePrimaryKey = "cache-primary-key", // Output from restore action
+    CacheRestoreKey = "cache-restore-key" // Output from restore action
 }
 
 export enum State {
@@ -24,3 +23,8 @@ export enum Events {
 }
 
 export const RefKey = "GITHUB_REF";
+
+export const stateToOutputMap = new Map<string, string>([
+    [State.CacheMatchedKey, Outputs.CacheRestoreKey],
+    [State.CachePrimaryKey, Outputs.CachePrimaryKey]
+]);
diff --git a/src/restoreImpl.ts b/src/restoreImpl.ts
index 3a57f90..dec2437 100644
--- a/src/restoreImpl.ts
+++ b/src/restoreImpl.ts
@@ -25,11 +25,7 @@ async function restoreImpl(
         }
 
         const primaryKey = core.getInput(Inputs.Key, { required: true });
-        stateProvider.setState(
-            State.CachePrimaryKey,
-            primaryKey,
-            Outputs.InputtedKey
-        );
+        stateProvider.setState(State.CachePrimaryKey, primaryKey);
 
         const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
         const cachePaths = utils.getInputAsArray(Inputs.Path, {
@@ -54,11 +50,7 @@ async function restoreImpl(
         }
 
         // Store the matched cache key in states
-        stateProvider.setState(
-            State.CacheMatchedKey,
-            cacheKey,
-            Outputs.MatchedKey
-        );
+        stateProvider.setState(State.CacheMatchedKey, cacheKey);
 
         const isExactKeyMatch = utils.isExactKeyMatch(
             core.getInput(Inputs.Key, { required: true }),
diff --git a/src/saveImpl.ts b/src/saveImpl.ts
index 67534c7..4c02c24 100644
--- a/src/saveImpl.ts
+++ b/src/saveImpl.ts
@@ -42,8 +42,7 @@ async function saveImpl(stateProvider: IStateProvider): Promise<void> {
 
         // If matched restore key is same as primary key, then do not save cache
         // NO-OP in case of SaveOnly action
-        const restoredKey =
-            stateProvider.getCacheState() || core.getInput(Inputs.RestoredKey);
+        const restoredKey = stateProvider.getCacheState();
 
         if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
             core.info(
diff --git a/src/stateProvider.ts b/src/stateProvider.ts
index 71cf33b..2578806 100644
--- a/src/stateProvider.ts
+++ b/src/stateProvider.ts
@@ -1,9 +1,9 @@
 import * as core from "@actions/core";
 
-import { State } from "./constants";
+import { State, stateToOutputMap } from "./constants";
 
 export interface IStateProvider {
-    setState(key: string, value: string, outputKey?: string): void;
+    setState(key: string, value: string): void;
     getState(key: string): string;
 
     getCacheState(): string | undefined;
@@ -21,7 +21,7 @@ class StateProviderBase implements IStateProvider {
     }
 
     // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
-    setState = (key: string, value: string, outputKey?: string) => {};
+    setState = (key: string, value: string) => {};
 
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     getState = (key: string) => "";
@@ -33,10 +33,8 @@ export class StateProvider extends StateProviderBase {
 }
 
 export class NullStateProvider extends StateProviderBase {
-    setState = (key: string, value: string, outputKey?: string) => {
-        if (outputKey) {
-            core.setOutput(outputKey, value);
-        }
+    setState = (key: string, value: string) => {
+        core.setOutput(stateToOutputMap.get(key) as string, value);
     };
     // eslint-disable-next-line @typescript-eslint/no-unused-vars
     getState = (key: string) => "";