文章

ROSD: Resource Oriented System Design

ROSD: Resource Oriented System Design

中文版本在此

Introduction

For a long time, the relational model based on relational databases has been the dominant paradigm for business data modeling, guiding the design and implementation of various business systems for decades. However, in engineering practice, its inherent design constraints have increasingly revealed rigid shortcomings in modern business scenarios characterized by rapid iteration. On one hand, the relational model requires a complete table structure to be determined at the early stages of development; subsequent changes in business requirements often involve complex database schema refactoring, incurring high development and maintenance costs. On the other hand, foreign key associations and join queries create strong coupling dependencies between tables, rendering the data structure monolithic. A change to a single table structure or field can easily trigger cascading code adjustments and logical risks. These issues make the relational model less capable of meeting the flexibility and extensibility needs of modern systems that must evolve continuously and iterate quickly.

To address these challenges, this paper proposes Resource Oriented System Design (ROSD). ROSD is a system design philosophy and methodology whose core goal is to provide a unified architectural guide for designing weakly coupled, highly flexible, and extensible systems. It abstracts the system state as a collection of various resource instances, and by granting resources independent lifecycles and employing design strategies such as weak reference associations, it fundamentally resolves the coupling and iteration difficulties inherent in the traditional relational model. Drawing on practical experience, this paper systematically introduces the core ideas and design principles of ROSD, and summarizes common design choices and key engineering implementation points.

From a macro-to-micro, top-down perspective, this paper roughly classifies computer systems into the following layers: (application) business system → language runtime system → operating system → hardware device system. The discussion primarily targets business system design, but since systems at all levels manage resources, the ideas should be generalizable to each layer.

Abstracting Things as “Resources”

ROSD and OOP

ROSD can be understood as an application of Object-Oriented Programming (OOP) ideas to macro-level system design. OOP has been widely proven in software engineering practice to effectively manage code complexity: objects are flexible, scalable, and easy to model real-world problems. OOP achieves high cohesion and low coupling—adding new functionality typically requires only adding new classes and objects without breaking existing structures. The relationship between ROSD and OOP is one of elevation and generalization:

  • Class in OOP ≈ Resource Type in ROSD
  • Object (instance) in OOP ≈ Resource Instance in ROSD

Consequently, the system design approach is unified: organize all system functionality by resource, just as objects encapsulate methods. The entire system state is the set of all resource instance states; all system behaviors are the operation primitives on various resources. On top of this, we build complex business logic that reduces diverse requirements to resource operation primitives. However complex the business logic, the fundamental capability boundaries of the system remain unchanged.

Thus, system design essentially becomes: defining a set of resource types and their operation primitives. The system designer’s job becomes: transforming “function-based requirement descriptions” into “resource-based system design”. Function-oriented design often becomes increasingly messy—functions become scattered, lack structure, and fail to abstract commonalities, leading to chaotic projects and systems. Resource-oriented design, however, first identifies the resources in the system, then attaches functions to corresponding resources. Ultimately, the system achieves true modularity and decoupling—this is the core design philosophy of ROSD.

“Resource-centricity” provides a stable thinking framework and guidelines. When faced with messy, diverse, and variably expressed requirements, we no longer feel lost or led by scattered functions; we have universal norms to follow. Designing according to these guidelines, on one hand, stays close to actual coding and implementation, providing clear direction for development. More importantly, it endows the system with good extensibility, enabling it to gracefully accommodate subsequent functional changes. At the same time, this unified design philosophy establishes a common conceptual foundation and discussion framework for team collaboration, multi-module coordination, and communication among different designers. Everyone works based on the same resource design logic; their respective modules naturally interoperate and coexist peacefully, avoiding coupling conflicts caused by inconsistent design ideas.

The impact of this shift is profound: the project code structure becomes clear, possessing the high cohesion and low coupling characteristic of OOP; the runtime system structure is naturally decoupled, so adding new functionality merely requires adding new resources without modifying existing business logic. In contrast, the relational model appears rigid: it forces you to think about what entities and relationships exist in the system and to eliminate data redundancy using primary keys, foreign keys, etc., ultimately linking everything into a monolithic block. In the early, requirements-uncertain, concept-vague stage, it is difficult to produce the entire data model design in one step; moreover, because the parts of the model are tightly coupled, subsequent iterations inevitably lead to large-scale refactoring of both the database and the codebase.

However, one crucial point must be emphasized: ROSD is a system design philosophy, independent of which database is used. Just as relational data modeling and system design do not necessarily mandate a relational database, ROSD does not forbid using a relational database. ROSD is a system design philosophy and methodology; it guides the design of the system itself, not the choice of database implementation—you can certainly use Postgres as the database for a ROSD business system, though writing the data layer code might be somewhat inconvenient.

ROSD and the Relational Model

To illustrate more intuitively how ROSD generalizes and elevates OOP, and contrasts sharply with the relational model, consider a typical scenario—differentiated requirements for the “User” entity across business departments.

In a complex system, “User” is often not a single, uniform concept but is shared by multiple business modules or departments. Different departments have completely different requirements:

  • Department A may only need simple authentication.
  • Department B may need contact information like address, phone number, and email.
  • Future departments C, D, etc., may each have independently evolving fields, logic, and constraints for “User”.

The relational model naturally forces us to initially build a global, shared large user table; otherwise, describing relationships such as primary and foreign keys between tables is difficult. Since everyone modifies it, this table can balloon to dozens or hundreds of fields. A requirement change from any department can trigger a full table refactoring, making the overall design highly coupled and extremely rigid. In short, the relational model demands a complete one-time system design and cannot tolerate gradual iteration of “get the system running first, then improve incrementally”, nor can it accommodate the natural pace of rapid evolution in business environments.

From ROSD’s perspective, the approach is entirely different. We do not force all business departments to share a single “User resource”. Instead, we consider that Department A’s User and Department B’s User are two completely independent resources. They may have some reference relationship, such as the same user ID or phone number, but this association is not mandatory, does not require uniform table structure, and does not introduce strong coupling constraints like foreign keys or cascading deletes. The two departments can independently design their own “User resources”, iterate, develop, and deploy independently, without interfering with each other.

From a system perspective, they function like two independent systems, only recognizing each other through a weak reference. The entire structure is not only stable but also highly extensible. When the business matures, we can merge, abstract, and unify these resources as needed, all as incremental optimization work that does not break the existing system.

In summary, ROSD allows us to:

  • Not pay in advance for uncertain future designs.
  • Not bear structural costs for unimplemented functionality ahead of time.
  • Add resources as needed, implementing locally first rather than building the entire system at once.
  • Decouple the system into parts, each evolving its own resource model independently, eventually converging naturally.

This design approach greatly enhances system extensibility, stability, and collaboration efficiency, enabling the system to evolve continuously in a flexible, incremental manner.

Operation Primitives for Resources

Operation primitives in ROSD can be likened to object methods in OOP. All operation primitives can be described by the following uniform form:

\[X:T.F(A) \rightarrow R\]

Where $X$ is the identifier of a resource instance, $T$ is the resource type, $F$ is the operation primitive, $A$ are the input parameters, and $R$ is the return result.

Similar to OOP, the set of all operation primitives in the system is finite and fully determined at design time. $T.F$ constitutes the identifier of the operation primitive, uniquely representing an element in the set.

Operation Primitive

The key difference from OOP is that, as shown above, because operation primitives need to work across systems, different systems can only exchange pure data. Therefore, $X$, $A$, and $R$ must all be serializable data, not something like a “reference” as in OOP.

Operation primitives can be implemented as internal system state or as encapsulations of functionality from other external systems—this is transparent to the operator. The operator can be a person or another external system; there is no distinction for the system. This structure is sufficient to model almost all real-world scenarios.

CIDE Classification

We classify operation primitives into four types:

  • C (Create): Creation operation.
  • I (Interact): Interaction operation.
  • D (Delete): Deletion operation.
  • E (Enumerate): Enumeration operation.

Information systems further subdivide I operations into read and write (Retrieve/Update), but information systems are just a special case of ROSD with no external side effects. In general, because operation primitives may encapsulate external systems, even an ostensibly read-only operation might involve writing in its implementation, so further subdividing I operations is unnecessary.

For D operations, common designs include:

  • $X:T.D() \rightarrow bool$

    Deletes resource instance $X$, returns whether the operation succeeded, i.e., whether $X$ existed before deletion.

  • $X:T.D() \rightarrow \bot$

    Deletes resource instance $X$ with no return result. Such a system either guarantees no operation when the resource does not exist, or requires the operator to ensure the resource exists (e.g., C’s free function). The latter is generally not valid in business scenarios because we cannot assume anything about external input and must fully validate all data.

  • $X:T.D() \rightarrow t_X$

    Deletes resource instance $X$ and simultaneously returns the state of $X$. This design is feasible only in pure information systems; in general systems, the state information of $X$ is usually tied to the system’s implementation and becomes meaningless outside the system’s context, so it cannot be returned to external parties.

For C operations, common designs include:

  • $T.C(A) \rightarrow X$

    Creates a resource instance with parameters $A$, returns the identifier $X$ of the new instance. Here, the operator does not know the actual value of $X$ before the C operation executes; we call such $X$ a posterior identifier.

  • $X:T.C(A) \rightarrow bool$

    Creates a resource instance, but the identifier $X$ of the new instance is directly specified. Since $X$ may already exist, the operation often returns whether it succeeded, i.e., whether $X$ existed before creation. Such $X$ is called a prior identifier.

  • $X’:T’.F(A) \rightarrow X$

    Resource instance $X$ is indirectly created by an operation primitive $F$ on another resource instance $X’$ (possibly of a different type $T’$). This is common in real scenarios, reflecting complex business logic.

E operations typically take the form $T.E() \rightarrow {X \dots }$, generally iterating over resource IDs rather than full resource objects (since the complete state of a resource object can be large and complex).

Enumeration (E) operations serve to satisfy the Complete Enumerability Principle described later. They are usually not guaranteed to be complete, especially in distributed systems—meaning the system allows you to iterate, but does not promise that you will see all resources exactly once without omissions; you must judge based on context (e.g., during maintenance downtime, completeness can be assumed).

Most other operation primitives in the system are I operations, designed according to business needs, and thus lack commonalities worth discussing.

Declarative Operations

Declarative operations directly specify the desired final state of the system “positionally”, rather than specifying the transformation process incrementally. The benefit of declarative operations is their conciseness, directness, and comprehensiveness; the drawback is that they refresh the entire system state each time, which can cause performance issues if most domains remain unchanged. This, in principle, means declarative operation interfaces can only complement imperative ones, not fully replace them.

A well-designed system should provide both types of operation interfaces. In general, upstream and downstream systems communicate efficiently using imperative operations, while declarative interfaces are used for state synchronization in low-frequency troubleshooting scenarios.

Resourceification of Spontaneous Processes

Operation primitives only allow resources to respond passively to external operations. However, in real scenarios, the ability of a system to spontaneously initiate actions is an extremely common and fundamental requirement. Therefore, this section focuses on: how to abstract a spontaneous process as a resource.

By “process,” we mean an independent piece of execution logic, such as a function or a processing flow. Processes themselves are typically transient or ephemeral—e.g., a function starts when called, and after execution finishes and results are returned, the stack frame is automatically released and destroyed. Resources, on the other hand, are long-lived, identifiable, and manageable entities. Our goal is to transform such one-time, transient processes into resources that the system can manage with lifecycles.

The most typical and representative scenarios are scheduled tasks and asynchronous jobs. Scheduled tasks are processes that execute at specified times or run periodically; asynchronous jobs are processes that continue running in the background (within the system) after an operation primitive completes. Both are essentially spontaneously running workflows, each requiring associating a piece of logic with a resource, or even making the logic itself a resource.

To achieve this resourceification of spontaneous processes, the system must include a task queue module. Generally, a single global task queue and timer serve as the unified driver. With this infrastructure, logic that would otherwise need to run spontaneously can be transformed into manageable resources. A “process resource” then essentially becomes: a task function + its bound invocation parameters.

Thus, it naturally acquires CID semantics:

  • Create (C): Not directly executing the process, but submitting a task to the task queue.
  • Interact (I): Modifying the task’s execution time, adjusting parameters, changing execution strategies, rescheduling, etc.
  • Delete (D): Canceling the task, removing it from the queue, and terminating its future execution.

Operation Granularity of Process Resources

Spontaneous processes are actions initiated and run autonomously by resources; operation primitives are passive operations initiated by external parties on resources. This inherent tension is a critical issue that system design cannot avoid. Here we must clarify a core design constraint and principle: a running process is often non-interruptible.

This non-interruptibility arises, on one hand, from the programming language level: most languages do not provide a mechanism to forcibly terminate a function or code fragment mid-execution. Once logic starts, it executes according to its predefined flow and cannot be arbitrarily terminated by external operations. On the other hand, even if the underlying process carrier supports termination (e.g., an independent OS process), such termination often does not meet design expectations—we cannot predict where in its execution the terminated process was, so termination introduces undefined behavior and often leaves various resources associated with the process in an inconsistent state, potentially causing system-wide anomalies.

Thus, “a running process cannot be interrupted” is not merely an implementation limitation but an inherent property of processes. Therefore, when designing a system, we must consider the operation granularity of process resources upfront: all processes must have predetermined points at which they can be operated upon. For one-off, short-lived processes, this granularity might be “before start” and “after end”, allowing operations like “cancel” or “rollback”, but not “abort”, as there are no abort points during execution.

For a potentially infinite-loop process, designing abort points within the process is essential; otherwise, the process would run forever, leading to resource leaks. Abort points split the process into a workflow of subprocesses, and we must ensure that on any possible execution path, each subprocess runs to completion in finite time [Process Finiteness Principle] ; otherwise, process resources risk becoming inoperable—a serious design flaw.

As a concrete example, suppose we need a system to periodically execute a check task in a loop. The following code is unacceptable:

1
2
3
4
def task():
    while True:
        sleep(1)
        check()

Such a task, while executing, cannot be safely terminated, modified, or rescheduled. Instead, an asynchronous recursion structure must be used:

1
2
3
def task():
    check()
    task_queue.submit(task, now() + 1)

Here, task itself contains no loop; instead, after completing its execution, the final step resubmits itself to the task queue. The task queue completes the loop, not the process internally hardcoding the loop. This way, the process can run periodically while remaining operable via CIDE primitives between runs.

Atomicity and Consistency Guarantees

In ROSD, the overall system state is the set of all resource instance states:

\[S = \{T^1_0, T^1_1, T^1_2, ..., T^2_0, T^2_1, T^2_2, ..., T^3_0, T^3_1, T^3_2, ...\}\]

The system must be designed to handle concurrent operations by multiple users. This consideration is not at the implementation level but the design level—i.e., what behavior the system should exhibit to users when operations are concurrent. This behavior is fundamentally reflected in the guarantees of operation atomicity and state consistency.

For low-level or microscopic systems, global atomicity is feasible; essentially, true concurrency is not allowed, and all external operations are serialized by a global lock, executed in some globally consistent order. However, this sacrifices concurrency and cannot support macroscopic systems, especially real-world business applications.

ROSD proposes a natural, practical, universal, and clear granularity level: only guarantee operation atomicity and state consistency on individual resource instances [Resource Atomicity Principle] . This design stipulates:

  • At any given time, at most one operation may be in progress on a resource instance $T^x_y$.
  • Therefore, all operation primitives on the same resource instance $T^x_y$ are mutually exclusive and sequentially consistent.
  • Concurrent operations on the same resource instance $T^x_y$ may be queued or rejected, depending on specific requirements.
  • Operations on different resource instances $T^x_y$, $T^z_w$ are concurrent, with no atomicity or consistency guarantees.

This idea is very close to the Monitor concept in object-oriented programming, elevated to the system design level rather than a programming language synchronization mechanism.

Is the granularity of “individual resource instance” too coarse? Should concurrent operations on the same resource instance be allowed? Our answer is “no”: because the concept of a resource is inherently highly flexible, scalable, and can be fine-grained or coarse-grained.

If you find that multiple operations within a “large resource” need to be parallelized, this often signals a resource granularity that is too coarse. The correct response is not to compromise the design but to split the large resource appropriately into smaller, interrelated fine-grained resources [Granularity Splitting Principle] . After splitting, each small resource still follows “single-resource internal atomicity, concurrency allowed between multiple resources”. The lack of cross-instance atomicity guarantees preserves the system’s overall concurrency while maintaining design rule uniformity and simplicity.

Prior/Posterior Identifiers

In ROSD, all external operations on the system must locate the target resource instance via the identifier $X$. We further assert that in system design, identifiers are not only exposed externally but are also typically the means by which resources reference each other internally. For example, if a project needs to record which user it belongs to, the project resource would store the user resource’s identifier. In the vast majority of cases, designing a separate internal identifier mechanism to hide implementation details from the outside is unnecessary. Following Occam’s razor, reusing the external identifier directly within the system is the simplest and most consistent choice.

The Dangling Reference Problem

The widespread mutual references between resources within a system directly raise a core issue that ROSD cannot avoid: dangling references. Since each resource instance’s CIDE operations are independent, there inevitably exist states where, after a resource instance is deleted, other resources that referenced it retain identifiers pointing to no longer existing instances—this is a dangling reference.

The occurrence of dangling references is not merely because ROSD lacks cross-resource-instance CIDE atomicity and consistency guarantees; it is an inherent design constraint: dependencies between resource types are naturally unidirectional [Unidirectional Dependency Principle] . For example, new resources introduced by new system features can depend on existing old resources, but old resources, when originally designed, could not possibly know which future resources might reference them. New resources will eventually become old resources, so we must assume by default that resources do not know which other resources reference them. Moreover, this unidirectional dependency is precisely the advantage that allows ROSD systems to evolve incrementally, distinguishing it from the monolithic relational design—not a defect.

One could design a bidirectional reference tracking system for ROSD, similar to the relational model, to delete or nullify associated resources when a resource instance is deleted. However, this presents two critical problems: first, such a bidirectional tracking system is not only difficult to implement but also incurs enormous computational and storage overhead at runtime; second, perhaps more fatally, it would mean that, by design, deleting a resource could affect modifications to other resources, thereby breaking the operational independence of each resource instance. In particular, considering atomicity and consistency guarantees on resources, such cascading modifications could easily lead to deadlocks, rollback issues, and other intractable problems. Therefore, tracking bidirectional references is generally not a good solution.

In contrast, ROSD recommends a crucial design principle: the system must be robust to invalid identifiers [Invalidity Robustness Principle] . Every operation primitive, when executed, must actively check the validity of all identifiers it uses and handle invalidation gracefully. This implies that all references between resources within the system are essentially weak references, even if both parties are internal. Furthermore, such identifier validity checking is necessarily feasible, because the system already checks validity for all external operations.

The pervasive use of weak references also confers an important property: since there are no strong references, any resource instance can be independently and forcibly created or deleted without being obstructed or entangled by other resources [Independent Control Principle] . This property is crucial for administrative systems. It ensures deterministic reclaimability in resource management: deleting a resource should genuinely release the physical resources it occupies, rather than leaving hidden residues due to obscure references.

As a counterexample, consider hard links in traditional file systems. Hard links constitute strong references, so even if a user performs a deletion operation, other links may prevent the file entity from being actually freed. This design is acceptable at the OS level because most resources at that level lack physical uniqueness: files are just associated with some storage space, and two disk sectors are not fundamentally different. However, at the higher business system level, each resource instance may be associated with a unique physical object, such as a person, an organization, or a company. In such cases, “cannot delete when you want to” is not just a waste of computational resources but could lead to serious real-world consequences. Therefore, ROSD’s weak references plus dangling reference robustness provide an effective guarantee for system controllability, reliability, and governance.

The Problem of Incorrect References with Prior Identifiers

If external parties are allowed to specify resource instance identifiers, the dangling reference problem further leads to a more severe issue: incorrect references. If an external system creates a new resource with the same identifier, this new resource may incorrectly inherit dangling references from the old resource. Such inheritance poses a serious risk of permission leakage in real business scenarios. For example, if an old user is deleted but their associated projects remain, and then a new user is created with the same identifier, the new user would immediately see the old user’s projects upon entering the system—effectively gaining unauthorized access to the old user’s data. This is clearly unacceptable.

A common mechanism to avoid this issue is soft deletion. Soft deletion does not actually remove the resource record from the system state (e.g., the database); instead, it adds a status flag, such as an is_deleted field. The deletion operation simply sets is_deleted to true. All resource operation primitives must also be modified to only execute when is_deleted is false. Since the old resource is not truly deleted, creating a resource with the same identifier is naturally prevented.

However, soft deletion has obvious limitations: it is essentially a disabling mechanism, not true deletion. One of the core semantics of deletion is to release the identifier of the original resource so it can be reused. This requires hiding the fact that “the identifier was once used,” which soft deletion cannot achieve. Such systems generally do not meet data compliance requirements. To prevent external probing of whether an identifier has been used, one would have to design additional identifier mechanisms that allow visible identifier reuse (e.g., adding suffixes, timestamps)—which effectively means the internal identifiers are no longer prior identifiers, contradicting our initial assumption.

Beyond that, prior identifiers themselves carry information leakage risk: external parties can guess common identifiers to enumerate resources within the system, which in some scenarios may lead to user privacy risks. In summary, designing resource identifiers as prior identifiers is highly discouraged. The resulting permission leakage, information leakage, and other issues are inherent flaws at the design principle level that cannot be fully mitigated by technical means. Therefore, prior identifier designs are generally not recommended in typical systems.

A classic example is email addresses. Email accounts are usually chosen by users—a typical prior identifier. Consequently, by knowing someone’s common naming style, one can attempt to register an account to test whether that email is already taken, thereby inferring whether the person has an account on the corresponding system. Today’s email systems are plagued by spam, partly due to this design flaw. In more private business systems, the mere existence of a resource is sensitive information; such leakage is no longer a minor issue but a serious security risk.

The Idempotency Problem with Posterior Identifiers

Since prior identifiers cannot be used, we must adopt posterior identifiers generated autonomously by the system. In this case, before a create operation, external parties cannot specify or predict what the identifier of the new resource will be. Because the identifier’s value is fully determined by the system, the system can completely solve dangling reference and incorrect reference problems by ensuring spatiotemporal uniqueness of identifier generation.

Posterior identifiers naturally lead to create operations not being idempotent: each create request generates a new resource instance without reusing, overwriting, or determining whether a corresponding resource already exists. In unstable communication environments, this can cause resource leaks. If a resource creation has successfully completed internally, but the new identifier fails to reach the external party due to network errors, the external party does not know that the resource has been created and has no simple or direct way to determine whether “the resource they wanted to create already exists.” Consequently, the caller’s most natural choice is to retry, sending another create request. To the external party, this typically does not affect functionality; they can continue working with the newly created resource. But inside the system, this creates a redundant resource that the external party neither needs nor knows about, which may linger indefinitely, unmanaged, eventually becoming system garbage.

This problem is not fatal, but it causes sustained waste of system resources, and long-term accumulation can affect system cleanliness and efficiency. Therefore, we propose an important design principle: all resources in the system must be completely enumerable [Complete Enumerability Principle] .

That is, the system must provide some means to iterate over all currently existing resources. This iteration does not require obtaining an absolutely complete, strongly consistent snapshot at a single moment—in concurrent systems, that is both difficult to achieve and of limited value, because the resource directory can change the next instant. We only require that the system eventually allow iteration and retrieval of all existing resource instances in a consistent manner.

While this principle does not fully prevent resource leaks due to creation retries, it ensures such leaks are discoverable and manageable. Subsequent regular inspections, garbage collection scripts, or manual reviews can identify unused, ownerless idle resources and destroy or release them, maintaining a clean and controllable system state.

A General Solution: Posterior Identifier + Unique Secondary Key

After discussing the issues of prior and posterior identifiers separately, we can propose a general, robust, and suitable-for-most-business-systems design. This scheme avoids the inherent defects of prior identifiers while partially addressing the shortcomings of posterior identifiers. It is ROSD’s recommended default identifier design.

The core idea:

  • The primary identifier of a resource remains posterior, generated internally by the system, not externally specifiable or predictable.
  • An additional, uniquely constrained “secondary key” field is added to provide idempotency.

The secondary key can be thought of as the resource’s “name” or “external alias,” but it does not have the immutability of the primary identifier; it is just an indexed, writable field on the resource. We recommend using a distributed unique ID algorithm (e.g., UUID4) to generate the primary identifier, which guarantees spatiotemporal uniqueness and is completely random, making it difficult for external parties to guess or predict.

This design solves multiple problems at once:

  1. Create operations can be idempotent.

    By passing the same secondary key on creation, the system can determine whether a corresponding resource already exists, avoiding duplicate creation and handling network timeouts, retries, and other abnormal scenarios.

  2. Resources support aliasing and renaming.

    Since resource references would otherwise rely on pure prior identifiers, names are hard to change once determined; with pure posterior identifiers, external parties find resources hard to remember and identify. Now, the secondary key serving as the resource name can be freely modified like any other field.

  3. Primary identifiers are inherently privacy-safe and secure.

    Because they are completely random and unpredictable, external parties cannot enumerate or guess which resources exist inside the system, avoiding information leakage at the primary identifier level. This inherent obscurity also provides supplementary protection beyond access control.

Because the secondary key has a uniqueness constraint, the vulnerability of probing resource existence via name-collision creation still exists. Several design strategies can further mitigate this:

  • Time-based validity

    Enforce secondary key uniqueness only for a period of time after creation to support idempotent retries; after the validity period expires, the system no longer enforces global uniqueness, allowing reuse of the secondary key.

  • Resource namespaces

    For indirectly created resources, prefix the identifier with a parent resource (e.g., user ID) to form independent namespaces. Uniqueness of the secondary key is guaranteed within a namespace, while different namespaces are mutually invisible.

  • Randomized secondary keys

    For scenarios with extreme privacy requirements, external parties can use random strings as secondary keys. In this case, the secondary key cannot serve as a resource name, so an additional field must be added as a human-readable alias.

Appropriate mitigation should be chosen based on specific security, availability, and implementation complexity requirements. With this design—posterior identifier plus unique secondary key—the previously mentioned core issues are largely resolved, making it the default design choice for general cases.

Implementation Considerations and Recommendations

Using a Key-Value Database

ROSD recommends using a key-value database (such as Redis) to store system state, as such databases align well with the single-resource-instance access pattern, offering low latency and good performance. In this structure:

  • The resource’s posterior primary identifier serves as the database key.
  • The resource’s state data serves as the corresponding value.

Most resource operations locate directly via the identifier, so performance is not an issue. However, some scenarios may involve database iteration, such as the secondary key design we discussed. To support reverse lookup from secondary key to primary identifier without an index, one would need to iterate over the entire database, which could become a performance bottleneck.

To avoid this bottleneck, we can maintain an additional reverse mapping cache from “secondary key → primary identifier”. This introduces a classic NoSQL problem: consistency of redundant data. The reverse index cache must remain consistent with the primary resource store; otherwise, anomalies such as missing lookups or incorrect identifiers may occur.

Such consistency issues do not require complex mechanisms like distributed transactions. Following ROSD’s own design suffices: maintain consistency across all operation primitives of the resource. Since only operation primitives directly access the database—no other write paths exist—careful implementation of primitives that maintain cache and primary data consistency is sufficient. Because we assume primitive execution is atomic (either complete success or complete failure), cache inconsistency should not occur in a correctly functioning system.

When extreme situations like system crashes occur, the cache and primary store may become inconsistent. To handle this, we rely on a core principle: the primary resource state is the sole authoritative source of truth [Source State Authority Principle] . The system should provide a fallback operation to rebuild all caches from the primary resource list, thereby fully repairing inconsistencies. Such operations are used only during anomaly recovery and do not affect normal runtime performance.

The “Shang” (上) Code Architecture

Code Architecture

From the preceding design, we can naturally derive a code organization structure shaped like the Chinese character “上” (shàng). As shown in the figure, the system code primarily consists of three parts:

  1. Primitive Layer

    This is the most core part of the system, defining the various resources and their operation primitives. Operation primitives are the only code that directly accesses the resource state database and external systems. All modifications to resource state and external calls are funneled through and implemented here, with consistency guaranteed. The primitive layer is generally stable once the system design is finalized, and it is the part of the system code with the highest requirements for stability and reliability.

  2. Business Layer

    All complex logic and business processes implemented by composing operation primitives should be extracted to the business layer. The business layer does not directly read/write the database or call external services; it only uses the primitive layer to accomplish functions. Compared to the primitive layer, business layer logic is irregular and changes more frequently, adapting to rapid business iteration. Separating it from the stable primitive layer ensures that low-level core code remains largely unchanged during iteration, enhancing system stability.

  3. Access Layer

    The access layer transforms various external inputs into runtime data that the system can process internally, and invokes the business layer’s capabilities. The system may have many diverse access layers, such as UI for human users, machine-oriented HTTP/gRPC APIs, etc. Different downstream systems, different programming languages, and different clients all connect through the access layer, ultimately invoking the business layer uniformly.

Overall, the “Shang”-shaped architecture embodies ROSD’s core ideas: the bottom layer funnels operations and remains stable; the upper layers are flexible and accommodate change; the strict boundary through operation primitives ensures both system consistency/robustness and strong extensibility/maintainability.

RESTful API Reference Design

ROSD systems are well-suited to RESTful APIs. This section provides a reference design using the user resource as an example.

We embed resource identifiers in URL patterns, e.g., /user/123 for user with ID 123; use nested paths to represent hierarchical relationships, e.g., /user/123/file/456 for file 456 belonging to user 123. We use the notation /user/<uid> to describe such URL patterns.

RESTful APIs typically use five HTTP methods to manipulate resources: GET, POST, DELETE, PUT, PATCH. We use POST for C operations, DELETE for D operations, and the other methods for I operations. In the design below, POST and DELETE are made batched, allowing creation or deletion of multiple resources in one request.

  • POST /user Batch create users

    The non-idempotent version’s request body is a JSON array, each element containing creation information, e.g.:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    [
      {
        "name": "Alice",
        "age": 20,
        "password": "123456"
      },
      {
        "name": "Bob",
        "age": 21,
        "password": "654321"
      }
    ]
    

    The response body is a JSON array giving the identifiers of the created resources:

    1
    2
    3
    4
    
    [
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
    ]
    

    The idempotent version’s request body is a JSON object, with resource secondary keys as keys and the same information as values:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    {
      "alice": {
        "name": "Alice",
        "age": 20,
        "password": "123456"
      },
      "bob": {
        "name": "Bob",
        "age": 21,
        "password": "654321"
      }
    }
    

    The response body is a JSON object, with resource secondary keys as keys and created resource identifiers as values:

    1
    2
    3
    4
    
    {
      "alice": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "bob": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
    }
    
  • DELETE /user Batch delete users

    The request body is a JSON array, each element being a user ID to delete, e.g.:

    1
    2
    3
    4
    
    [
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
      "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
    ]
    

    The response body is a boolean array indicating whether each deletion succeeded:

    1
    
    [true, false]
    
  • GET /user List all users

    The request body is empty; the response body is a JSON dictionary providing brief information for all users:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
    {
      "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx": {
        "name": "Alice",
        "age": 20
      },
      "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy": {
        "name": "Bob",
        "age": 21
      }
    }
    
  • GET /user/<uid> Get user information

    The request body must be empty (as GET requests have no body). The response body is a JSON object representing detailed user information, e.g., GET /user/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx returns:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    {
      "name": "Alice",
      "age": 20,
      "status": "active",
      "files": ["2000", "2001"],
      "created-at": "2020-01-01T00:00:00Z",
      "last-login": "2020-01-01T00:00:00Z",
      "last-modified": "2020-01-01T00:00:00Z"
    }
    

    What constitutes “brief information” versus “detailed information” depends on the specific business context. The key consideration is that user lists can be long, so each entry must be concise; individual users can contain more information without length concerns.

  • PUT /user/<uid> Update user information

    The request body is a JSON object representing the information to update, e.g.:

    1
    2
    3
    4
    
    {
      "name": "Alice",
      "age": 21
    }
    

    The response body can be null or the updated user data.

  • PUT /user/<uid>/password Change user password

    Passwords are sensitive data and are not convenient to modify via a general PUT. A separate API is recommended—this also facilitates monitoring of sensitive operations. The request body is a JSON object representing the password change, e.g.:

    1
    2
    3
    4
    
    {
      "old": "123456",
      "new": "654321"
    }
    

    The response body can be null.

  • PATCH /user/<uid>/notice Send a notification to the user

    Sending a notification is not a pure data operation, so the PATCH method is used. The request body is a JSON object representing the notification content, e.g.:

    1
    2
    3
    4
    
    {
      "title": "Hello",
      "content": "World"
    }
    

    The response body can be null.

Design Case: “Quota” as a Resource

Below we use a complete, intuitive example to demonstrate how to apply ROSD thinking to gracefully extend system functionality—adding resource quota limits for users.

Assume the system already has mature user management capabilities. Now we need to add quota control: limit the total amount of various resources each user can use, and monitor usage.

Option A: Directly Extend the User Resource

This is the most intuitive approach: add several fields to the existing user resource, each recording for a controlled resource type:

  • Total quota limit
  • Used amount
  • Remaining available amount

However, this approach has clear drawbacks: the user is a core system resource; touching it can affect a large amount of code. Modifying the user structure may require widespread code changes, high risk, and broad impact. It further requires database schema changes, potentially involving data migration and downtime risks.

If problems arise during the process, the stability of the entire user system is affected. Clearly, the new functionality becomes highly coupled with existing logic, contradicting incremental iteration principles.

Option B: Abstract as a New Independent Resource

Following ROSD, we can design the quota itself as an independent resource. Each “quota resource” needs only four pieces of information:

  1. Resource name: what type of resource.
  2. Ownership information: which user this quota belongs to.
  3. Total limit: the maximum allowed amount of that resource type.
  4. Usage status: current used amount.

This design yields clear advantages:

  • Completely non-invasive to the existing user system

    Quotas are independent resources—no modifications to the user table, no changes to user logic, no impact on existing code. The new functionality is fully decoupled from the existing system and has no side effects on existing users.

  • Naturally backward compatible

    Before a quota resource is explicitly created for a user, the user is subject to no quota restrictions, behaving exactly as before the system upgrade. Restrictions take effect only when a quota is explicitly created, enabling on-demand enablement and smooth upgrades.

  • Supports flexible, multi-dimensional quota policies

    Multiple quota resources can be created for the same user to control different resource types. A policy like “reject if any quota is exceeded” can be implemented, achieving fine-grained, composable limit rules with strong extensibility.

  • Monitoring logic can be implemented via process resourceification

    Quota checking, violation detection, and alerting logic need not be embedded in business flows. Instead, they can be encapsulated in an autonomous background task, turned into a periodically executing job via process resourceification. The system periodically scans quota states and automatically sends emails or in-app notifications when violations occur. The entire logic is autonomous, pluggable, and non-intrusive to the main flow.

This case shows that ROSD enables non-destructive, non-invasive, incremental, and extensible system function iteration. No need to refactor the underlying layers, migrate data, or risk modifying core modules. Simply adding a new resource type safely and elegantly integrates complex functionality into the system—a true best practice of ROSD design thinking in real engineering.

Conclusion

This paper has introduced ROSD, which generalizes OOP concepts to the macro system design level, using resource types and resource instances to uniformly model system state and behavior. It standardizes resource interaction via CIDE operation primitives and transforms spontaneous processes into manageable resource entities to achieve system autonomy. The design adheres to principles such as resource atomicity, unidirectional dependency, and weak references, adopts a hybrid identifier scheme of posterior identifiers plus unique secondary keys to balance security, idempotency, and extensibility, and relies on key-value storage with a “Shang”-shaped layered architecture to achieve high cohesion and low coupling. Compared to the traditional relational model, ROSD supports incremental iteration and flexible extension, allowing efficient addition of system functionality without invading existing modules, providing a stable, universal, and engineering-friendly design paradigm for complex business systems.

References

The development of resource-oriented thinking can be traced back to several classic theories and practices: Ken Thompson and Dennis Ritchie’s “everything is a file” philosophy in Unix during the 1970s-1980s, abstracting various system entities as unified resources with standard interfaces; Tim Berners-Lee’s “everything is a resource” with URIs as unique identifiers when inventing the WWW in 1989; Roy Fielding’s formalization of the resource model in his 2000 doctoral dissertation “Architectural Styles and the Design of Network-based Software Architectures,” where he proposed the REST architectural style; Hewlett-Packard Labs’ Resource-Oriented Computing (ROC) theory in the 2000s, deepening resource-oriented computation models; the rise of NoSQL key-value databases such as Redis and Cassandra from the late 2000s to the 2010s, providing naturally suitable storage infrastructure for resource models; and the Resource-Oriented Architecture (ROA) proposed around 2004 by Paul Prescod, Stefan Tilkov, and others, further expanding the application boundaries of resource-oriented thinking. My proposal of ROSD was directly inspired by the Kubernetes API, and can be seen as a comprehension and synthesis of these ideas.

本文由作者按照 CC BY 4.0 进行授权