diff --git a/project.clj b/project.clj
index 2d4c3db0aca890c46c032e036ed3596775cbdb5d..c38b1496340722b63b51254b470343a52347577b 100644
--- a/project.clj
+++ b/project.clj
@@ -120,7 +120,8 @@
    [org.eclipse.jetty/jetty-server "9.4.15.v20190215"]                ; We require JDK 8 which allows us to run Jetty 9.4, ring-jetty-adapter runs on 1.7 which forces an older version
    [ring/ring-json "0.4.0"]                                           ; Ring middleware for reading/writing JSON automatically
    [stencil "0.5.0"]                                                  ; Mustache templates for Clojure
-   [toucan "1.11.0" :exclusions [org.clojure/java.jdbc honeysql]]]    ; Model layer, hydration, and DB utilities
+   [toucan "1.11.0" :exclusions [org.clojure/java.jdbc honeysql]]     ; Model layer, hydration, and DB utilities
+   [weavejester/dependency "0.2.1"]]                                  ; Dependency graphs and topological sorting
 
   :main ^:skip-aot metabase.core
 
diff --git a/src/metabase/query_processor/middleware/fetch_source_query.clj b/src/metabase/query_processor/middleware/fetch_source_query.clj
index 2813a3fa193dcc49e48641e866da4670fc0364da..e292b47f76ad74ba419b99097e494dfe8c77c22b 100644
--- a/src/metabase/query_processor/middleware/fetch_source_query.clj
+++ b/src/metabase/query_processor/middleware/fetch_source_query.clj
@@ -33,7 +33,8 @@
              [i18n :refer [trs tru]]
              [schema :as su]]
             [schema.core :as s]
-            [toucan.db :as db]))
+            [toucan.db :as db]
+            [weavejester.dependency :as dep]))
 
 ;; These next two schemas are for validating the intermediate stages of the middleware. We don't need to validate the
 ;; entire query
@@ -146,38 +147,47 @@
      (dissoc m :source-table)
      source-query-and-metadata)))
 
-(defn- check-for-circular-references
-  "Make sure we aren't trying to resolve circular references such as a card whose source query is itself."
-  [card-id already-resolved]
-  (when (contains? (set already-resolved) card-id)
-    (throw (ex-info (str (tru "Cannot resolve source query for Card {0}: circular references between source queries."
-                              card-id))
-             {:resolving card-id, :already-resolved already-resolved}))))
-
 (defn- resolve-all*
+  [m]
+  (mbql.u/replace m
+    map-with-card-id-source-table?
+    ;; if this is a map that has a Card ID `:source-table`, resolve that (replacing it with the appropriate
+    ;; `:source-query`, then recurse and resolve any nested-nested queries that need to be resolved too
+    (let [resolved (resolve-one &match)]
+      ;; wrap the recursive call in a try-catch; if the recursive resolution fails, add context about the
+      ;; resolution that were we in the process of
+      (try
+        (resolve-all* resolved)
+        (catch Throwable e
+          (throw (ex-info (.getMessage e)
+                   {:resolving &match, :resolved resolved}
+                   e)))))))
+
+(defn- check-for-circular-references
+  "Check that there are no circular dependencies among source cards. This is equivalent to
+   finding a topological sort of the dependency graph.
+   https://en.wikipedia.org/wiki/Topological_sorting"
   ([m]
-   (resolve-all* m nil))
-
-  ([m already-resolved]
-   (mbql.u/replace m
-     map-with-card-id-source-table?
-     ;; if this is a map that has a Card ID `:source-table`, resolve that (replacing it with the appropriate
-     ;; `:source-query`, then recurse and resolve any nested-nested queries that need to be resolved too
-     (let [source-table-str (:source-table &match)
-           card-id          (source-table-str->card-id source-table-str)]
-       (check-for-circular-references card-id already-resolved)
-       ;; ok, now actually resolve the Card ID, then recursively resolve all the Card ID source tables inside the
-       ;; newly resolved source query
-       ;;
-       (let [resolved (resolve-one &match)]
-         ;; wrap the recursive call in a try-catch; if the recursive resolution fails, add context about the
-         ;; resolution that were we in the process of
-         (try
-           (resolve-all* resolved (conj already-resolved card-id))
-           (catch Throwable e
-             (throw (ex-info (.getMessage e)
-                      {:resolving &match, :resolved resolved}
-                      e)))))))))
+   (check-for-circular-references (dep/graph) m)
+   m)
+  ([g m]
+   (transduce (comp (filter map-with-card-id-source-table?)
+                    (map (comp card-id->source-query-and-metadata
+                               source-table-str->card-id
+                               :source-table)))
+              (fn
+                ([] g)
+                ([g source-query]
+                 (-> g
+                     (dep/depend m source-query)
+                     ;; Recursive call will circuit break the moment there's a cycle, so no
+                     ;; danger of unbounded recursion.
+                     (check-for-circular-references source-query)))
+                ([g]
+                 ;; This will throw if there's a cycle
+                 (dep/topo-sort g)
+                 g))
+              (tree-seq coll? identity m))))
 
 (defn- copy-source-query-database-ids
   "If `m` has the saved questions virtual `:database` ID, (recursively) look for actual resolved Database IDs in the
@@ -209,7 +219,9 @@
   "Recursively replace all Card ID source tables in `m` with resolved `:source-query` and `:source-metadata`. Since
   the `:database` is only useful for top-level source queries, we'll remove it from all other levels."
   [query :- su/Map]
-  (-> (resolve-all* query)
+  (-> query
+      check-for-circular-references
+      resolve-all*
       copy-source-query-database-ids
       remove-unneeded-database-ids))
 
diff --git a/test/metabase/query_processor/middleware/fetch_source_query_test.clj b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
index a2a28bc3bf5a4f41559f9cfad68de58316ec3aad..4848653c6837dd22489bf28159aaa6b4f464ea36 100644
--- a/test/metabase/query_processor/middleware/fetch_source_query_test.clj
+++ b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
@@ -236,3 +236,22 @@
                            (str "Failed to save Card:" e)))]
         (or save-error
             (resolve-card-id-source-tables (circular-source-query card-1-id)))))))
+
+;; Alow complex dependency topologies such as:
+;;
+;;   A
+;:   | \
+;;   B  |
+;;   | /
+;;   C
+;;
+(expect
+  (tt/with-temp* [Card [{card-1-id :id} {:dataset_query (data/mbql-query venues)}]
+                  Card [{card-2-id :id} {:dataset_query (data/mbql-query nil
+                                                          {:source-table (str "card__" card-1-id)})}]]
+    (resolve-card-id-source-tables
+     (data/mbql-query nil
+       {:source-table (str "card__" card-1-id)
+        :joins        [{:alias        "c"
+                        :source-table (str "card__" card-2-id)
+                        :condition    [:= *ID/Number &c.venues.id]}]}))))