diff --git a/docs/framework_concepts/building_graphs_cpp.md b/docs/framework_concepts/building_graphs_cpp.md
index f6f5441fa..c415711e5 100644
--- a/docs/framework_concepts/building_graphs_cpp.md
+++ b/docs/framework_concepts/building_graphs_cpp.md
@@ -409,3 +409,187 @@ CalculatorGraphConfig BuildGraph() {
return graph.GetConfig();
}
```
+
+### Keep nodes decoupled from each other
+
+In MediaPipe, packet streams and side packets are as meaningful as processing
+nodes. And any node input requirements and output products are expressed clearly
+and independently in terms of the streams and side packets it consumes and
+produces.
+
+```c++ {.bad}
+CalculatorGraphConfig BuildGraph() {
+ Graph graph;
+
+ // Inputs.
+ Stream a = graph.In(0).Cast();
+
+ auto& node1 = graph.AddNode("Calculator1");
+ a.ConnectTo(node1.In("INPUT"));
+
+ auto& node2 = graph.AddNode("Calculator2");
+ node1.Out("OUTPUT").ConnectTo(node2.In("INPUT")); // Bad.
+
+ auto& node3 = graph.AddNode("Calculator3");
+ node1.Out("OUTPUT").ConnectTo(node3.In("INPUT_B")); // Bad.
+ node2.Out("OUTPUT").ConnectTo(node3.In("INPUT_C")); // Bad.
+
+ auto& node4 = graph.AddNode("Calculator4");
+ node1.Out("OUTPUT").ConnectTo(node4.In("INPUT_B")); // Bad.
+ node2.Out("OUTPUT").ConnectTo(node4.In("INPUT_C")); // Bad.
+ node3.Out("OUTPUT").ConnectTo(node4.In("INPUT_D")); // Bad.
+
+ // Outputs.
+ node1.Out("OUTPUT").SetName("b").ConnectTo(graph.Out(0)); // Bad.
+ node2.Out("OUTPUT").SetName("c").ConnectTo(graph.Out(1)); // Bad.
+ node3.Out("OUTPUT").SetName("d").ConnectTo(graph.Out(2)); // Bad.
+ node4.Out("OUTPUT").SetName("e").ConnectTo(graph.Out(3)); // Bad.
+
+ return graph.GetConfig();
+}
+```
+
+In the above code:
+
+* Nodes are coupled to each other, e.g. `node4` knows where its inputs are
+ coming from (`node1`, `node2`, `node3`) and it complicates refactoring,
+ maintenance and code reuse
+ * Such usage pattern is a downgrade from proto representation, where nodes
+ are decoupled by default.
+* `node#.Out("OUTPUT")` calls are duplicated and readability suffers as you
+ could use cleaner names instead and also provide an actual type.
+
+So, to fix the above issues you can write the following graph construction code:
+
+```c++ {.good}
+CalculatorGraphConfig BuildGraph() {
+ Graph graph;
+
+ // Inputs.
+ Stream a = graph.In(0).Cast();
+
+ // `node1` usage is limited to 3 lines below.
+ auto& node1 = graph.AddNode("Calculator1");
+ a.ConnectTo(node1.In("INPUT"));
+ Stream b = node1.Out("OUTPUT").Cast();
+
+ // `node2` usage is limited to 3 lines below.
+ auto& node2 = graph.AddNode("Calculator2");
+ b.ConnectTo(node2.In("INPUT"));
+ Stream c = node2.Out("OUTPUT").Cast();
+
+ // `node3` usage is limited to 4 lines below.
+ auto& node3 = graph.AddNode("Calculator3");
+ b.ConnectTo(node3.In("INPUT_B"));
+ c.ConnectTo(node3.In("INPUT_C"));
+ Stream d = node3.Out("OUTPUT").Cast();
+
+ // `node4` usage is limited to 5 lines below.
+ auto& node4 = graph.AddNode("Calculator4");
+ b.ConnectTo(node4.In("INPUT_B"));
+ c.ConnectTo(node4.In("INPUT_C"));
+ d.ConnectTo(node4.In("INPUT_D"));
+ Stream e = node4.Out("OUTPUT").Cast();
+
+ // Outputs.
+ b.SetName("b").ConnectTo(graph.Out(0));
+ c.SetName("c").ConnectTo(graph.Out(1));
+ d.SetName("d").ConnectTo(graph.Out(2));
+ e.SetName("e").ConnectTo(graph.Out(3));
+
+ return graph.GetConfig();
+}
+```
+
+Now, if needed, you can easily remove `node1` and make `b` a graph input and no
+updates are needed to `node2`, `node3`, `node4` (same as in proto representation
+by the way), because they are decoupled from each other.
+
+Overall, the above code replicates the proto graph more closely:
+
+```proto
+input_stream: "a"
+
+node {
+ calculator: "Calculator1"
+ input_stream: "INPUT:a"
+ output_stream: "OUTPUT:b"
+}
+
+node {
+ calculator: "Calculator2"
+ input_stream: "INPUT:b"
+ output_stream: "OUTPUT:C"
+}
+
+node {
+ calculator: "Calculator3"
+ input_stream: "INPUT_B:b"
+ input_stream: "INPUT_C:c"
+ output_stream: "OUTPUT:d"
+}
+
+node {
+ calculator: "Calculator4"
+ input_stream: "INPUT_B:b"
+ input_stream: "INPUT_C:c"
+ input_stream: "INPUT_D:d"
+ output_stream: "OUTPUT:e"
+}
+
+output_stream: "b"
+output_stream: "c"
+output_stream: "d"
+output_stream: "e"
+```
+
+On top of that, now you can extract utility functions for further reuse in other graphs:
+
+```c++ {.good}
+Stream RunCalculator1(Stream a, Graph& graph) {
+ auto& node = graph.AddNode("Calculator1");
+ a.ConnectTo(node.In("INPUT"));
+ return node.Out("OUTPUT").Cast();
+}
+
+Stream RunCalculator2(Stream b, Graph& graph) {
+ auto& node = graph.AddNode("Calculator2");
+ b.ConnectTo(node.In("INPUT"));
+ return node.Out("OUTPUT").Cast();
+}
+
+Stream RunCalculator3(Stream b, Stream c, Graph& graph) {
+ auto& node = graph.AddNode("Calculator3");
+ b.ConnectTo(node.In("INPUT_B"));
+ c.ConnectTo(node.In("INPUT_C"));
+ return node.Out("OUTPUT").Cast();
+}
+
+Stream RunCalculator4(Stream b, Stream c, Stream d, Graph& graph) {
+ auto& node = graph.AddNode("Calculator4");
+ b.ConnectTo(node.In("INPUT_B"));
+ c.ConnectTo(node.In("INPUT_C"));
+ d.ConnectTo(node.In("INPUT_D"));
+ return node.Out("OUTPUT").Cast();
+}
+
+CalculatorGraphConfig BuildGraph() {
+ Graph graph;
+
+ // Inputs.
+ Stream a = graph.In(0).Cast();
+
+ Stream b = RunCalculator1(a, graph);
+ Stream c = RunCalculator2(b, graph);
+ Stream d = RunCalculator3(b, c, graph);
+ Stream e = RunCalculator4(b, c, d, graph);
+
+ // Outputs.
+ b.SetName("b").ConnectTo(graph.Out(0));
+ c.SetName("c").ConnectTo(graph.Out(1));
+ d.SetName("d").ConnectTo(graph.Out(2));
+ e.SetName("e").ConnectTo(graph.Out(3));
+
+ return graph.GetConfig();
+}
+```