You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/06/11 16:11:37 UTC
(opendal) 01/01: feat(website): Add a concepts page with an interactive mental model
This is an automated email from the ASF dual-hosted git repository.
Xuanwo pushed a commit to branch xuanwo/website-concepts
in repository https://gitbox.apache.org/repos/asf/opendal.git
commit c2911f43f5ea39c58a4ec1605be57a69e7a49370
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Fri Jun 12 00:11:25 2026 +0800
feat(website): Add a concepts page with an interactive mental model
---
website/docs/01-overview.md | 3 +
website/docs/03-concepts.mdx | 70 ++++
website/src/components/concepts/MentalModel.jsx | 128 +++++++
website/src/components/concepts/samples.js | 96 ++++++
website/src/components/concepts/styles.module.css | 390 ++++++++++++++++++++++
5 files changed, 687 insertions(+)
diff --git a/website/docs/01-overview.md b/website/docs/01-overview.md
index 2cdd55833..047c2a90a 100644
--- a/website/docs/01-overview.md
+++ b/website/docs/01-overview.md
@@ -8,3 +8,6 @@ slug: /
OpenDAL represents **Open** **D**ata **A**ccess **L**ayer. Our vision is [**One Layer, All Storage.**](./02-vision.md)

+
+New to OpenDAL? Start with [Concepts](./03-concepts.mdx) — the four ideas
+behind every OpenDAL API: service, operator, layer, and operation.
diff --git a/website/docs/03-concepts.mdx b/website/docs/03-concepts.mdx
new file mode 100644
index 000000000..468a71d23
--- /dev/null
+++ b/website/docs/03-concepts.mdx
@@ -0,0 +1,70 @@
+---
+sidebar_label: Concepts
+description: The four ideas behind every OpenDAL API — service, operator, layer, and operation.
+---
+
+import MentalModel from "@site/src/components/concepts/MentalModel";
+import CodeTabs from "@site/src/components/landing/CodeTabs";
+import { conceptSamples } from "@site/src/components/concepts/samples";
+
+# Concepts
+
+Every OpenDAL API, in every language, follows one model:
+
+**Configure a service, build an operator, optionally wrap it with layers, then
+call operations.**
+
+<MentalModel />
+
+## Service
+
+A service is a storage backend: S3, Google Cloud Storage, Azure Blob, a local
+filesystem, an in-memory store — [more than 50 in total](/services).
+
+You never talk to a service directly. You describe it with plain configuration
+— bucket, root, endpoint, credentials — and OpenDAL builds the client for you.
+Because a service is only configuration, switching backends is a configuration
+change, not a code change: `s3` in production, `fs` on your laptop, `memory`
+in tests.
+
+## Operator
+
+The operator is the entry point for everything. It is built from one service's
+configuration, and every storage call in your application goes through it.
+
+Operators are lightweight handles: cheap to create, and safe to share across
+threads and tasks. There are no connections to pool or cursors to manage. One
+operator maps to one service and one root path — to work with two buckets,
+build two operators.
+
+## Layer
+
+A layer adds behavior around an operator: automatic retry, logging, timeouts,
+metrics, concurrency limits.
+
+Layers nest like an onion: every operation passes through every layer on the
+way in and on the way out. Cross-cutting concerns are composed once at startup
+instead of being scattered through your code. Applying a layer returns a new
+operator and leaves the original untouched.
+
+## Operation
+
+Operations are the verbs: `read`, `write`, `stat`, `list`, `delete`, `copy`,
+`rename`, and friends. They are the same on every service, so code written
+against one backend runs against any other.
+
+Paths follow one rule everywhere: they are always relative to the operator's
+root, and a trailing `/` means a directory. `logs/app/` is a directory;
+`logs/app` is a file.
+
+## One model, every language
+
+The same four steps, in the language you already ship:
+
+<CodeTabs samples={conceptSamples} title="service → operator → layer → operation" />
+
+## Next steps
+
+- [Services](/services) — every supported backend and its configuration.
+- [Core](./10-core.mdx) — the Rust core library that powers everything.
+- [Bindings](/docs/category/bindings) — use OpenDAL from your language.
diff --git a/website/src/components/concepts/MentalModel.jsx b/website/src/components/concepts/MentalModel.jsx
new file mode 100644
index 000000000..89c68b662
--- /dev/null
+++ b/website/src/components/concepts/MentalModel.jsx
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ */
+
+import React, { useState } from "react";
+import styles from "./styles.module.css";
+
+// The OpenDAL mental model in one picture: an application calls an operator,
+// the operator is wrapped by layers, and the operator talks to one configured
+// service. Selecting a concept highlights its place in the diagram; the
+// one-line hint lives in the window title bar. The diagram is decorative
+// (aria-hidden) — the page prose carries the same information for assistive
+// technology.
+const CONCEPTS = [
+ {
+ id: "service",
+ index: "01",
+ name: "service",
+ hint: "a backend described by plain config",
+ },
+ {
+ id: "operator",
+ index: "02",
+ name: "operator",
+ hint: "one handle for every storage call",
+ },
+ {
+ id: "layer",
+ index: "03",
+ name: "layer",
+ hint: "wraps the operator, sees every call",
+ },
+ {
+ id: "operation",
+ index: "04",
+ name: "operation",
+ hint: "the same verbs on every backend",
+ },
+];
+
+export default function MentalModel() {
+ const [active, setActive] = useState("service");
+ const current = CONCEPTS.find((concept) => concept.id === active);
+
+ return (
+ <figure className={styles.model} data-active={active}>
+ <div className={styles.windowBar}>
+ <div className={styles.windowDots} aria-hidden="true">
+ <span />
+ <span />
+ <span />
+ </div>
+ <span className={styles.windowTitle}>
+ {current.index} {current.name} — {current.hint}
+ </span>
+ </div>
+ <div className={styles.diagram} aria-hidden="true">
+ <div className={`${styles.gridLayer} odl-grid-bg`} />
+ <div className={styles.backend}>
+ <div className={styles.service}>
+ <span className={styles.svcName}>
+ <span className={styles.kind}>service · </span>s3
+ </span>
+ <code className={styles.code}>bucket = data</code>
+ </div>
+ <span className={styles.alts}>fs · gcs · azblob · 50+ more</span>
+ </div>
+ <span className={`${styles.flow} ${styles.flowBuild}`}>
+ <span className={styles.flowLabel}>build</span>
+ </span>
+ <div className={styles.ringOuter}>
+ <span className={styles.ringLabel}>
+ <span className={styles.kind}>layer · </span>retry
+ </span>
+ <div className={styles.ringInner}>
+ <span className={styles.ringLabel}>
+ <span className={styles.kind}>layer · </span>logging
+ </span>
+ <div className={styles.core}>operator</div>
+ </div>
+ </div>
+ <span className={`${styles.flow} ${styles.flowCall}`}>
+ <span className={styles.flowLabel}>call</span>
+ </span>
+ <div className={styles.app}>
+ <span className={styles.comment}>{"// your app"}</span>
+ <code className={styles.code}>op.read("hello.txt")</code>
+ </div>
+ </div>
+ <figcaption className={styles.legend}>
+ {CONCEPTS.map((concept) => {
+ const selected = active === concept.id;
+ return (
+ <button
+ key={concept.id}
+ type="button"
+ className={`${styles.legendItem} ${
+ selected ? styles.legendItemActive : ""
+ }`}
+ aria-pressed={selected}
+ onMouseEnter={() => setActive(concept.id)}
+ onFocus={() => setActive(concept.id)}
+ onClick={() => setActive(concept.id)}
+ >
+ <span className={styles.legendIndex}>{concept.index}</span>
+ <span className={styles.legendName}>{concept.name}</span>
+ </button>
+ );
+ })}
+ </figcaption>
+ </figure>
+ );
+}
diff --git a/website/src/components/concepts/samples.js b/website/src/components/concepts/samples.js
new file mode 100644
index 000000000..f80e492f3
--- /dev/null
+++ b/website/src/components/concepts/samples.js
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ */
+
+// The four-step mental model — service, operator, layer, operation — written
+// out in each binding's real API so the concepts page never drifts from the
+// libraries it describes.
+export const conceptSamples = [
+ {
+ id: "rust",
+ label: "Rust",
+ language: "rust",
+ code: `use opendal::layers::RetryLayer;
+use opendal::services::S3;
+use opendal::Operator;
+
+// 1. Describe the service.
+let builder = S3::default().bucket("data");
+
+// 2. Build the operator and 3. wrap it with layers.
+let op = Operator::new(builder)?
+ .layer(RetryLayer::new())
+ .finish();
+
+// 4. Call operations.
+op.write("hello.txt", "Hello, World!").await?;
+let bytes = op.read("hello.txt").await?;`,
+ },
+ {
+ id: "python",
+ label: "Python",
+ language: "python",
+ code: `import opendal
+
+# 1. Describe the service and 2. build the operator.
+op = opendal.Operator("s3", bucket="data")
+
+# 3. Wrap it with layers.
+op = op.layer(opendal.layers.RetryLayer())
+
+# 4. Call operations.
+op.write("hello.txt", b"Hello, World!")
+data = op.read("hello.txt")`,
+ },
+ {
+ id: "java",
+ label: "Java",
+ language: "java",
+ code: `import org.apache.opendal.AsyncOperator;
+import org.apache.opendal.layer.RetryLayer;
+import java.util.Map;
+
+// 1. Describe the service and 2. build the operator,
+// 3. wrapped with layers.
+var conf = Map.of("bucket", "data");
+try (var op = AsyncOperator.of("s3", conf)
+ .layer(RetryLayer.builder().build())) {
+ // 4. Call operations.
+ op.write("hello.txt", "Hello, World!").join();
+ byte[] data = op.read("hello.txt").join();
+}`,
+ },
+ {
+ id: "node",
+ label: "Node.js",
+ language: "javascript",
+ code: `import { Operator, RetryLayer } from "opendal";
+
+// 1. Describe the service and 2. build the operator.
+let op = new Operator("s3", { bucket: "data" });
+
+// 3. Wrap it with layers.
+const retry = new RetryLayer();
+retry.maxTimes = 3;
+op = op.layer(retry.build());
+
+// 4. Call operations.
+await op.write("hello.txt", "Hello, World!");
+const data = await op.read("hello.txt");`,
+ },
+];
diff --git a/website/src/components/concepts/styles.module.css b/website/src/components/concepts/styles.module.css
new file mode 100644
index 000000000..cb462c11e
--- /dev/null
+++ b/website/src/components/concepts/styles.module.css
@@ -0,0 +1,390 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License 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.
+ */
+
+/* ===== Window frame — same chrome as the landing code windows ============ */
+.model {
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ background: var(--odl-surface);
+ box-shadow: var(--odl-shadow-lg);
+ overflow: hidden;
+ margin: var(--odl-space-6) 0;
+}
+
+.windowBar {
+ display: flex;
+ align-items: center;
+ gap: var(--odl-space-3);
+ padding: var(--odl-space-3) var(--odl-space-4);
+ border-bottom: 1px solid var(--odl-border);
+ background: var(--odl-surface-2);
+}
+.windowDots {
+ display: flex;
+ gap: 6px;
+}
+.windowDots span {
+ width: 11px;
+ height: 11px;
+ border-radius: var(--odl-radius-pill);
+ background: var(--odl-border-strong);
+}
+.windowTitle {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+/* ===== Diagram =========================================================== */
+.diagram {
+ position: relative;
+ display: flex;
+ align-items: center;
+ gap: var(--odl-space-4);
+ padding: clamp(2rem, 1.5rem + 2vw, 3rem) clamp(1.25rem, 1rem + 1.5vw, 2rem);
+ background: var(--odl-bg);
+}
+/* Blueprint grid fading out from the top, as on the landing hero. */
+.gridLayer {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ -webkit-mask-image: radial-gradient(
+ 110% 100% at 50% 0%,
+ #000 0%,
+ transparent 78%
+ );
+ mask-image: radial-gradient(110% 100% at 50% 0%, #000 0%, transparent 78%);
+}
+
+.app,
+.flow,
+.ringOuter,
+.backend {
+ position: relative;
+ transition: opacity var(--odl-dur) var(--odl-ease);
+}
+
+.app,
+.service {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: var(--odl-space-3) var(--odl-space-4);
+ border: 1px solid var(--odl-border-strong);
+ border-radius: var(--odl-radius-md);
+ background: var(--odl-surface);
+ box-shadow: var(--odl-shadow-sm);
+ transition: border-color var(--odl-dur) var(--odl-ease);
+}
+
+.comment {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-2xs);
+ color: var(--odl-fg-subtle);
+ white-space: nowrap;
+}
+.code {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg);
+ background: none;
+ border: 0;
+ padding: 0;
+ white-space: nowrap;
+}
+
+/* Connectors: hairlines converging on the operator. The build arrow points
+ right (config flows in), the call arrow points left (the app calls in). */
+.flow {
+ flex: 1 1 1.5rem;
+ min-width: 1.25rem;
+ height: 1px;
+ background: var(--odl-border-strong);
+}
+.flow::after {
+ content: "";
+ position: absolute;
+ width: 7px;
+ height: 7px;
+ border-top: 1px solid var(--odl-border-strong);
+ border-right: 1px solid var(--odl-border-strong);
+ transition: border-color var(--odl-dur) var(--odl-ease);
+}
+.flowBuild::after {
+ right: 0;
+ top: -3.5px;
+ transform: rotate(45deg);
+}
+.flowCall::after {
+ left: 0;
+ top: -3.5px;
+ transform: rotate(-135deg);
+}
+.flowLabel {
+ position: absolute;
+ left: 50%;
+ bottom: 5px;
+ transform: translateX(-50%);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-2xs);
+ letter-spacing: 0.04em;
+ color: var(--odl-fg-subtle);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+
+/* Layers wrap the operator as light, dashed envelopes; the operator stays
+ the only solid mass in the middle. */
+.ringOuter,
+.ringInner {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: var(--odl-space-2) var(--odl-space-3) var(--odl-space-3);
+ border: 1px dashed var(--odl-border-strong);
+ transition: border-color var(--odl-dur) var(--odl-ease);
+}
+.ringOuter {
+ border-radius: var(--odl-radius-lg);
+ background: transparent;
+}
+.ringInner {
+ border-radius: var(--odl-radius-md);
+ background: var(--odl-surface);
+ box-shadow: var(--odl-shadow-sm);
+}
+.ringLabel {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-2xs);
+ color: var(--odl-fg-muted);
+ white-space: nowrap;
+ transition: color var(--odl-dur) var(--odl-ease),
+ opacity var(--odl-dur) var(--odl-ease);
+}
+/* "kind ·" prefix inside labels: quiet type, louder instance. */
+.kind {
+ font-weight: 400;
+ color: var(--odl-fg-subtle);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+
+/* The operator is the solid "bar" at the center of the model. */
+.core {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-sm);
+ font-weight: 600;
+ text-align: center;
+ padding: var(--odl-space-3) var(--odl-space-6);
+ border-radius: var(--odl-radius-sm);
+ background: var(--odl-bar);
+ color: var(--odl-bg);
+ box-shadow: var(--odl-shadow-md);
+ transition: opacity var(--odl-dur) var(--odl-ease),
+ box-shadow var(--odl-dur) var(--odl-ease);
+}
+
+.backend {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: var(--odl-space-2);
+}
+.svcName {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ font-weight: 600;
+ color: var(--odl-fg-strong);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+.alts {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-2xs);
+ color: var(--odl-fg-subtle);
+ padding-left: 2px;
+ white-space: nowrap;
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+
+/* ===== Highlight states ================================================== */
+/* service */
+.model[data-active="service"] .app,
+.model[data-active="service"] .flow,
+.model[data-active="service"] .ringOuter {
+ opacity: 0.35;
+}
+.model[data-active="service"] .service {
+ border-color: var(--odl-accent);
+}
+.model[data-active="service"] .svcName,
+.model[data-active="service"] .svcName .kind {
+ color: var(--odl-accent);
+}
+.model[data-active="service"] .alts {
+ color: var(--odl-fg-muted);
+}
+
+/* operator */
+.model[data-active="operator"] .app,
+.model[data-active="operator"] .flow,
+.model[data-active="operator"] .backend {
+ opacity: 0.35;
+}
+.model[data-active="operator"] .ringOuter,
+.model[data-active="operator"] .ringInner {
+ border-color: var(--odl-border);
+}
+.model[data-active="operator"] .ringLabel {
+ opacity: 0.4;
+}
+.model[data-active="operator"] .core {
+ box-shadow: 0 0 0 2px var(--odl-accent), var(--odl-shadow-md);
+}
+
+/* layer */
+.model[data-active="layer"] .app,
+.model[data-active="layer"] .flow,
+.model[data-active="layer"] .backend {
+ opacity: 0.35;
+}
+.model[data-active="layer"] .ringOuter,
+.model[data-active="layer"] .ringInner {
+ border-color: var(--odl-accent);
+}
+.model[data-active="layer"] .ringLabel,
+.model[data-active="layer"] .ringLabel .kind {
+ color: var(--odl-accent);
+}
+.model[data-active="layer"] .core {
+ opacity: 0.5;
+}
+
+/* operation */
+.model[data-active="operation"] .app {
+ border-color: var(--odl-accent);
+}
+.model[data-active="operation"] .flow {
+ background: var(--odl-accent);
+}
+.model[data-active="operation"] .flow::after {
+ border-color: var(--odl-accent);
+}
+.model[data-active="operation"] .flowLabel {
+ color: var(--odl-accent);
+}
+
+/* ===== Legend — one compact row, tab-style accent indicator ============= */
+.legend {
+ display: flex;
+ flex-wrap: wrap;
+ margin: 0;
+ border-top: 1px solid var(--odl-border);
+ background: var(--odl-surface-2);
+}
+.legendItem {
+ appearance: none;
+ font: inherit;
+ flex: 1 1 25%;
+ display: flex;
+ align-items: baseline;
+ justify-content: center;
+ gap: var(--odl-space-2);
+ padding: var(--odl-space-3) var(--odl-space-2);
+ border: 0;
+ border-top: 2px solid transparent;
+ background: transparent;
+ cursor: pointer;
+ transition: background-color var(--odl-dur) var(--odl-ease),
+ border-color var(--odl-dur) var(--odl-ease);
+}
+.legendItem:hover {
+ background: var(--odl-surface);
+}
+.legendItemActive {
+ background: var(--odl-surface);
+ border-top-color: var(--odl-accent);
+}
+.legendIndex {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-2xs);
+ font-weight: 500;
+ letter-spacing: var(--odl-tracking-label);
+ color: var(--odl-fg-subtle);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+.legendItemActive .legendIndex {
+ color: var(--odl-accent);
+}
+.legendName {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-sm);
+ font-weight: 600;
+ color: var(--odl-fg-muted);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+.legendItemActive .legendName {
+ color: var(--odl-fg-strong);
+}
+
+/* ===== Responsive ======================================================== */
+@media (max-width: 768px) {
+ .diagram {
+ flex-direction: column;
+ align-items: stretch;
+ }
+ .app,
+ .ringOuter,
+ .backend {
+ align-self: center;
+ }
+ .flow {
+ flex: 0 0 auto;
+ align-self: center;
+ width: 1px;
+ min-width: 0;
+ height: 1.5rem;
+ min-height: 1.5rem;
+ }
+ /* Stacked: build points down into the operator, call points up from the
+ app at the bottom. */
+ .flowBuild::after {
+ right: -3.5px;
+ top: auto;
+ bottom: 0;
+ transform: rotate(135deg);
+ }
+ .flowCall::after {
+ left: -3.5px;
+ top: 0;
+ transform: rotate(-45deg);
+ }
+ .flowLabel {
+ left: 10px;
+ bottom: auto;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ .legendItem {
+ flex-basis: 50%;
+ }
+}