Architectural Design

Declared vs. Implemented Architecture

  • Declared architecture — the PlantUML diagrams passed to architectural_design (static, dynamic, public_api, internal_api). This is what your architecture is supposed to look like: the components, units, and interfaces you intend to build, and how they should relate to each other.

  • Implemented architecture — the actual Bazel targets that get compiled and tested: unit(implementation = [...]) wraps the real source files, component(components = [...]) groups those units, and dependable_element(components = [...]) assembles the complete SEooC. This is what your architecture actually is.

Because these two views are authored independently, they can drift apart. Therefore rules_score implements an automatic architecture consistency check that runs at bazel build/bazel test time: every component or unit that appears in dependable_element.components must also appear, under the same name, in the static PlantUML diagram declared via architectural_design.static — and vice versa. A mismatch fails the build. See General Information for the full list of automatic validations rules_score performs.

Overview and Hierarchy

Software in rules_score is structured in three levels:

dependable_element   (SEooC — complete Safety Element out of Context)
└── component        (groups units; owns component-level integration tests and requirements)
    ├── unit         (smallest independently verifiable architectural element: implementation + unit tests)
    └── component    (components can be nested for deeper hierarchies)
        └── unit

Two rules apply:

  • unit targets must always be wrapped in a component — they cannot be placed directly under dependable_element.

  • component targets can be nested: a component may contain other components as well as units, allowing arbitrary depth.

Below the different levels there are multiple views which present the architecture from different perspectives:

  • Static — the structural organisation: which components and units exist, how they nest, and how they depend on each other. Validated against the Bazel model at build time.

  • Dynamic — behavioural sequences, state transitions, and activity flows. Documentation only, not validated against Bazel targets.

  • Public API — the interfaces the SEooC exposes to its environment, linked to safety analysis via FailureMode.interface.

  • Internal API — interfaces exposed between components inside the SEooC that are not part of the public boundary.

Determining Components and Units

The Bazel rules and the consistency check only verify that your declared and implemented structure match — they cannot tell you whether the structure is good. Deciding what becomes a component and what becomes a unit is a design activity.

Start from the requirements and the public interface, then decompose top-down: the dependable_element is fixed by the SEooC boundary (its public API), and you refine it into components and units until every leaf is small enough to be implemented and tested by one owner.

What makes a unit

A unit is the smallest architectural element that is independently verifiable. Model something as a unit when it satisfies all of the following:

  • Single responsibility — it does one thing; you can state its purpose in a single sentence without using “and”.

  • Independently verifiable — its behaviour can be fully covered by unit tests through a narrow interface, without standing up the rest of the SEooC.

  • Cohesive implementation — its source files (the cc_library behind unit.implementation) change together and share the same data.

  • One owner — a single team/person is responsible for its design and tests.

  • Backed by a unit design — its internal class structure is documented and validated against the code via Software Unit Design.

If a unit’s class diagram grows several unrelated clusters of classes, or its unit tests split into groups that never share fixtures, it is really two units.

What makes a component

A component groups units (and possibly sub-components) that collaborate to deliver a coherent piece of feature behaviour. Introduce a component when:

  • Several units together realise one feature or provide one internal interface to the rest of the SEooC.

  • The grouping owns behaviour that only emerges from unit interaction — captured by component-level integration tests and component requirements (CompReq).

  • It gives you a stable boundary you can allocate requirements to and reason about in the safety analysis.

Nest a component inside another component only when the inner grouping has its own meaningful interface and requirements; do not nest purely to mirror source folders.

Deciding the boundaries

Use these heuristics — most are the classic high-cohesion / low-coupling rules applied to the rules_score element levels:

  • Cohesion first — put things that change together and share data in the same element; split things that change for different reasons.

  • Minimise the interface — prefer a decomposition that yields the fewest, narrowest interfaces between elements. A boundary that needs a wide, chatty interface is usually in the wrong place.

  • Follow the requirement allocation — a CompReq is allocated to exactly one component. If a candidate requirement naturally splits across two groups, that is a component boundary; if it lands entirely inside one group, keep it together.

  • Match the failure-containment goal — a component/unit boundary is also a boundary for the safety analysis. Draw boundaries so that a failure can be argued about, and a control measure placed, at a single element (see Dependability Analysis).

  • Keep units testable in isolation — if you cannot unit-test a candidate unit without a second unit present, either merge them or introduce an interface (internal API) so the dependency can be substituted.

Public vs. internal interfaces

  • An interface is public API when it is part of the SEooC’s contract with its environment — it is bound from the <<SEooC>> element and feeds FailureMode.interface traceability. Keep the public API as small as the requirements allow; every public method is a contract with the user and a safety-analysis entry point.

  • An interface is internal API when it exists only between components/units inside the SEooC. Model it inside the owning element’s namespace. Promote an interface from internal to public only when an external requirement forces it.

Common anti-patterns

  • Folder-driven decomposition — creating a component per source directory instead of per feature/interface. Structure follows responsibility, not layout.

  • God unit — one unit that accumulates unrelated responsibilities because it was the first one created. Split as soon as a second responsibility appears.

  • Anaemic component — a component that only forwards calls and owns no integration tests or requirements. Either give it a real boundary or flatten it.

  • Leaky public API — exposing an interface publicly for convenience. It then drags in unnecessary failure modes and AoUs.

Static Architecture

The static view describes the structural organisation of your software: what components and units exist, how they relate to each other, and which dependencies they carry. It is the primary input for the architecture consistency check.

PlantUML

Write a PlantUML class or component diagram that names every component and unit from your Bazel BUILD file.

@startuml SeoocExample_StaticDesign

package "Safety Software SEooC Example" as safety_software_seooc_example <<SEooC>> {
    component "ComponentExample" as component_example <<component>> {
        component "Unit 1" as unit_1 <<unit>>
        component "Unit 2" as unit_2 <<unit>>
        component "Sub Component Example" as sub_component_example <<component>>

        interface "InternalInterface" as InternalInterface
        unit_1 -l-( InternalInterface
        unit_2 )-r- InternalInterface
    }
}

interface "SampleLibraryAPI" as SampleLibraryAPI

safety_software_seooc_example )-d- SampleLibraryAPI

@enduml

Valid PlantUML Definitions

The validator identifies elements by their stereotype, not by the PlantUML keyword used. Both package and component keywords are accepted at each level.

Stereotype

Valid PlantUML keywords

Meaning

Bazel rule

<<SEooC>>

package, component

Safety Element out of Context boundary

dependable_element

<<component>>

component, package

Architectural component

component

<<unit>>

component, package

Leaf implementation unit

unit

Interface Bindings

Any component-type element (<<SEooC>>, <<component>>, or <<unit>>) can bind directly to an interface using the lollipop syntax — also a dedicated port can be drawn.

@startuml static_design

package "Safety Software SEooC Example" as safety_software_seooc_example <<SEooC>> {
    component "ComponentExample" as component_example <<component>> {
        component "Unit 1" as unit_1 <<unit>>
        component "Unit 2" as unit_2 <<unit>>
        component "Sub Component Example" as sub_component_example <<component>>

        interface "InternalInterface" as InternalInterface
        unit_1 -l-( InternalInterface
        unit_2 )-r- InternalInterface
    }
}

interface "SampleLibraryAPI" as SampleLibraryAPI

safety_software_seooc_example )-d- SampleLibraryAPI

@enduml

Named Ports (alternative)

When an element needs an explicitly named, standalone binding point — for example to distinguish multiple provided interfaces without attaching them to a specific child unit — declare a portin / portout inside the <<SEooC>> or <<component>> element instead of binding directly on a child element:

@startuml SeoocExample_StaticDesign

package "Safety Software SEooC Example" as safety_software_seooc_example <<SEooC>> {
    component "ComponentExample" as component_example <<component>> {
        component "Unit 1" as unit_1 <<unit>>
    }

    portin  " " as p_required   ' required interface port
    portout " " as p_public     ' provided interface port
}

interface "RequiredInterface"  as RequiredInterface
interface "SampleLibraryAPI"   as SampleLibraryAPI

p_required -( RequiredInterface : requires
p_public   )- SampleLibraryAPI : provides

@enduml

Bazel

architectural_design

load("@score_tooling//bazel/rules/rules_score:rules_score.bzl", "architectural_design")

architectural_design(
    name   = "my_arch",
    static = ["static_design.puml"],  # the static diagram above
    dynamic = ["sequence_design.puml"],
)

unit

load("@score_tooling//bazel/rules/rules_score:rules_score.bzl", "unit")

# Unit 1
cc_library(name = "unit_1_lib",  srcs = ["foo.cpp"],      hdrs = ["foo.h"])
cc_test   (name = "unit_1_test", srcs = ["foo_test.cpp"], deps = [":unit_1_lib"])

unit(
    name           = "unit_1",
    unit_design    = ["//unit_1/docs:unit_design"],
    implementation = [":unit_1_lib"],
    tests          = [":unit_1_test"],
)

# Unit 2
cc_library(name = "unit_2_lib",  srcs = ["bar.cpp"],      hdrs = ["bar.h"])
cc_test   (name = "unit_2_test", srcs = ["bar_test.cpp"], deps = [":unit_2_lib"])

unit(
    name           = "unit_2",
    unit_design    = ["//unit_2/docs:unit_design"],
    implementation = [":unit_2_lib"],
    tests          = [":unit_2_test"],
)

component

load("@score_tooling//bazel/rules/rules_score:rules_score.bzl",
     "component", "component_requirements")

component_requirements(
    name = "component_requirements",
    srcs = ["component_requirements.trlc"],
    deps = [":feature_requirements"],
)

# The component maps to ComponentExample in the PlantUML diagram
component(
    name         = "component_example",
    requirements = [":component_requirements"],
    components   = [":unit_1", ":unit_2"],
    tests        = [],
)

Dynamic Architecture

The dynamic view describes behavioural aspects — sequences of interactions, state transitions, and activity flows. Dynamic diagrams document how your software behaves at runtime. They are not validated against the Bazel structure at build time.

PlantUML

@startuml SeoocExample_DynamicDesign

participant "Unit 1" as unit_1 <<unit>>
participant "Unit 2" as unit_2 <<unit>>

unit_1 -> unit_2 : GetData()
return Data*

@enduml

Bazel

architectural_design(
    name    = "my_arch",
    static  = ["static_design.puml"],
    dynamic = ["sequence.puml"],
)

Public API

The public API view describes the interface your SEooC exposes to its environment. They define a clear interface for the user of the dependable element and state which functions of the dependable element are carrying the safety related information.

As a proof for their safety relevance for each public method a FMEA should be carried out. This is documented by linking each method to a safety analysis: FailureMode records reference interface items by name (via the interface field), enabling traceability from each failure mode back to the architecture.

PlantUML

@startuml SeoocExample_PublicApi

namespace safety_software_seooc_example {
    interface "SampleLibraryAPI" as SampleLibraryAPI {
        + GetNumber()
    }
}

@enduml

Bazel

architectural_design(
    name       = "my_arch",
    public_api = ["public_api.puml"],
)

The public_api attribute also generates traceability items that can be referenced by fmea targets (see Dependability Analysis) via the arch_design attribute.

Internal API

The internal API view documents interfaces exposed between components inside the SEooC that are not part of the public boundary — for example, a service one component provides to a sibling component. These diagrams are parsed like static/dynamic views, but their FlatBuffers output is tracked separately via ArchitecturalDesignInfo.internal_api for downstream validation. Unlike public_api, they do not generate failure-mode traceability items.

PlantUML

Model the interface inside the namespace of the owning component so its fully-qualified name reflects the containment hierarchy:

@startuml SeoocExample_InternalApi

namespace safety_software_seooc_example {
  namespace component_example {
    interface "InternalInterface" as InternalInterface <<interface>>{
      {abstract} GetData()
    }
  }
}

@enduml

Bazel

architectural_design(
    name         = "my_arch",
    internal_api = ["internal_api.puml"],
)

RST and Markdown Wrappers

When you want to combine a diagram with text, create an RST or Markdown file that embeds the diagram using the .. uml:: directive (RST) or the MyST equivalent.

RST wrapper example:

Static Architecture
-------------------

The following diagram shows the component structure of MySeooc.

.. uml:: MySeooc_StaticDesign.puml

Include both the wrapper file and the referenced .puml file in the same Bazel list — the build needs both:

architectural_design(
    name   = "my_arch",
    static = [
        "static_design.rst",          # wrapper with prose
        "MySeooc_StaticDesign.puml",  # diagram referenced by the wrapper
    ],
)

Rule Reference: architectural_design

For the complete architectural_design attribute reference, see architectural_design in the rule index.