diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CachingSymbolProvider.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CachingSymbolProvider.java
new file mode 100644
index 00000000000..fa5f16f8ddf
--- /dev/null
+++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/CachingSymbolProvider.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License").
+ * You may not use this file except in compliance with the License.
+ * A copy of the License is located at
+ *
+ *  http://aws.amazon.com/apache2.0
+ *
+ * or in the "license" file accompanying this file. This file is distributed
+ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+
+package software.amazon.smithy.codegen.core;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+
+/**
+ * Caches the results of calling {@code toSymbol} and {@code toMemberName}.
+ */
+final class CachingSymbolProvider implements SymbolProvider {
+
+    private final SymbolProvider delegate;
+    private final ConcurrentMap<ShapeId, Symbol> symbolCache = new ConcurrentHashMap<>();
+    private final ConcurrentMap<ShapeId, String> memberCache = new ConcurrentHashMap<>();
+
+    CachingSymbolProvider(SymbolProvider delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public Symbol toSymbol(Shape shape) {
+        return symbolCache.computeIfAbsent(shape.toShapeId(), id -> delegate.toSymbol(shape));
+    }
+
+    @Override
+    public String toMemberName(Shape shape) {
+        return memberCache.computeIfAbsent(shape.toShapeId(), id -> delegate.toMemberName(shape));
+    }
+}
diff --git a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolProvider.java b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolProvider.java
index ec5c00fd802..cb8285090b8 100644
--- a/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolProvider.java
+++ b/codegen/smithy-codegen-core/src/main/java/software/amazon/smithy/codegen/core/SymbolProvider.java
@@ -73,4 +73,25 @@ public interface SymbolProvider {
     default String toMemberName(Shape shape) {
         return shape.getId().getMember().orElseGet(() -> StringUtils.uncapitalize(shape.getId().getName()));
     }
+
+    /**
+     * Decorates a {@code SymbolProvider} with a cache and returns the
+     * decorated {@code SymbolProvider}.
+     *
+     * <p>The results of calling {@code toSymbol} and {@code toMemberName}
+     * on {@code delegate} are cached using a thread-safe cache.
+     *
+     * <pre>
+     * {@code
+     * SymbolProvider delegate = createComplexProvider(myModel);
+     * SymbolProvider cachingProvider = SymbolProvider.cache(delegate);
+     * }
+     * </pre>
+     *
+     * @param delegate Symbol provider to wrap and cache its results.
+     * @return Returns the wrapped SymbolProvider.
+     */
+    static SymbolProvider cache(SymbolProvider delegate) {
+        return new CachingSymbolProvider(delegate);
+    }
 }
diff --git a/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/CachingSymbolProviderTest.java b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/CachingSymbolProviderTest.java
new file mode 100644
index 00000000000..f6d18434776
--- /dev/null
+++ b/codegen/smithy-codegen-core/src/test/java/software/amazon/smithy/codegen/core/CachingSymbolProviderTest.java
@@ -0,0 +1,41 @@
+package software.amazon.smithy.codegen.core;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.equalTo;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeId;
+import software.amazon.smithy.model.shapes.StringShape;
+
+public class CachingSymbolProviderTest {
+    @Test
+    public void cachesResults() {
+        List<ShapeId> calls = new ArrayList<>();
+
+        SymbolProvider delegate = shape -> {
+            calls.add(shape.getId());
+            return Symbol.builder().name(shape.getId().getName()).build();
+        };
+
+        SymbolProvider cache = SymbolProvider.cache(delegate);
+
+        Shape a = StringShape.builder().id("foo.baz#A").build();
+        Shape b = StringShape.builder().id("foo.baz#B").build();
+        Shape c = MemberShape.builder().id("foo.baz#C$c").target(a).build();
+
+        assertThat(cache.toSymbol(a).getName(), equalTo("A"));
+        assertThat(cache.toSymbol(b).getName(), equalTo("B"));
+        assertThat(cache.toSymbol(c).getName(), equalTo("C"));
+        assertThat(cache.toSymbol(a).getName(), equalTo("A"));
+        assertThat(cache.toSymbol(b).getName(), equalTo("B"));
+        assertThat(cache.toSymbol(c).getName(), equalTo("C"));
+        assertThat(cache.toMemberName(c), equalTo("c"));
+
+        assertThat(calls, containsInAnyOrder(a.getId(), b.getId(), c.getId()));
+    }
+}