Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save khandar-william/7a618ce99fcdb27f6f354568aa1f702d to your computer and use it in GitHub Desktop.

Select an option

Save khandar-william/7a618ce99fcdb27f6f354568aa1f702d to your computer and use it in GitHub Desktop.

Video: https://www.youtube.com/watch?v=2JNJjqHrQ1g

Slides: https://studylib.net/doc/13120169/expressing-architectural-design-intent-in-code-george-fai...

Transcript formatted:

1. Agile and software architecture are compatible

  • Agile development does not mean that software architecture is unnecessary.

  • The speaker compares this to Toyota:

    • Lean development helps Toyota work efficiently.
    • Automotive engineering knowledge helps Toyota build good cars.
    • You need both.
  • Similarly, good software development needs:

    • Agile practices such as short iterations, testing, and pair programming.
    • Technical knowledge about design and architecture.
  • Agile practices do not replace software design knowledge.

2. Separate three things: architect, architecting, and architecture

The speaker says that "architecture" is often confused with three different concepts:

  1. Architect

    • A role in an organization.
    • You may have a dedicated architect, or the developers may collectively perform the role.
  2. Architecting

    • The process used to make architectural decisions.
    • This can happen through Scrum, design discussions, experimentation, etc.
  3. Architecture

    • The resulting technical structure of the system.
    • This exists whether or not anyone explicitly planned it.

Important: A system has an architecture even if nobody designed it upfront.


3. What is "design intent"?

  • Design intent is the developers' understanding of:

    • How the system should work.
    • Why it is structured that way.
    • What constraints must remain true.
    • How the system should evolve.
  • Some of this knowledge exists in:

    • Developers' heads.
    • Whiteboards.
    • Conversations.
    • Architecture diagrams.
    • Design documents.
  • When the system is implemented, some of this knowledge is lost between the design and the code.

  • Later developers must reverse-engineer the original intent from the code.

The goal of the talk is therefore:

Make as much design intent as practical visible in the source code.


4. "The code is the design" is not completely true

  • The common Agile idea is to "trust the code" or "use the source code as the ultimate truth."

  • The speaker argues that the code does not contain all design knowledge.

  • Example:

    • Two programs can be mathematically/functionally equivalent.
    • One can have meaningful names and structure.
    • The other can be heavily obfuscated.
    • The computer can execute both, but humans understand one much better.
  • Code also does not necessarily contain:

    • Design rationale.
    • Architectural constraints.
    • Performance requirements.
    • Quality requirements.
    • Rules about what the system must not do.

Therefore:

Code is an important representation of design, but it is not a complete representation of design intent.


5. A major problem: programming languages describe solutions, not forbidden solutions

  • Programming languages are designed primarily to tell computers what to do.

  • Architecture also contains many statements about what must not happen.

  • Example:

    • Suppose a system uses a cache.
    • All access to the data must go through the cache.
    • Bypassing the cache could make the cache inconsistent.
  • You may understand this rule as an architectural constraint, but ordinary source code may not explicitly state:

"Nobody is allowed to bypass this cache."

  • This is an example of intentional knowledge that is difficult to directly encode in normal programming languages.

6. Extensional vs. intentional architecture knowledge

The speaker introduces two categories:

Extensional

Things that can be explicitly named or enumerated.

Examples:

  • Module A
  • Module B
  • Component C
  • Connector X
  • Port Y

These are relatively easy to represent in source code.

Intentional

Rules that apply to things generally.

Examples:

  • All filters communicate through pipes.
  • Components must not depend on certain modules.
  • A particular operation must be asynchronous.
  • A component must satisfy a performance requirement.
  • Certain dependencies are forbidden.

These are much harder to express directly.

Key distinction:

Extensional information says what exists. Intentional information says what must be true about what exists.


7. Object-oriented programming already uses "model in code"

  • Object-oriented design became popular partly because it allows programmers to represent domain concepts directly in code.
  • For example:
Airplane
  ├── takeOff()
  ├── land()
  └── persist()
  • The code structure can resemble the domain being modeled.

  • Meaningful:

    • Class names
    • Method names
    • Variable names
    • Responsibilities help humans understand the system.

This is what the speaker calls the "model in code" principle:

Expressing models in code improves comprehension and evolvability.


8. But "model in code" creates technical debt

  • If your code represents your understanding of the domain, then the code can become inconsistent with reality.

  • For example:

    • The domain model says an airplane can do X.
    • The real domain changes.
    • The code still represents the old model.
  • The speaker describes technical debt roughly as:

The difference between how we believe the system works and how the code actually works.

  • Therefore, modeling in code is both:

    • Useful, because the model becomes visible.
    • Dangerous, because the model can become outdated.

9. What is software architecture?

The speaker uses a definition similar to:

Software architecture is the set of structures needed to reason about a system, including its elements, relationships, and properties.

Important consequences:

  • Every software system has an architecture.
  • You do not need to design the architecture upfront.
  • A system can evolve into its architecture.
  • Architecture is observable from the resulting system.

10. Architecture affects quality attributes

Architecture influences qualities such as:

  • Performance
  • Latency
  • Modifiability
  • Usability
  • Testability
  • Reliability
  • Scalability

The speaker prefers the term quality attributes or extra-functional requirements rather than "non-functional requirements."

Example:

  • A centralized architecture may have different reliability characteristics from a peer-to-peer architecture.
  • Skype's peer-to-peer architecture gives it properties that would be difficult to obtain from a purely centralized design.

11. Not every architecture is a layered architecture

  • Developers often draw everything as a traditional layered architecture.

  • The speaker warns against forcing every system into a layer diagram.

  • Other architectural styles include:

    • Pipe-and-filter
    • Client-server
    • Peer-to-peer
    • MapReduce
    • N-tier
    • Big ball of mud
  • The architecture should reflect the actual structure of the system, not the diagram style that is most familiar.


12. Architecture has multiple views

The speaker discusses architectural views, particularly:

  1. Module view

    • How source code is divided into modules.
  2. Runtime view

    • What components exist and communicate when the program runs.
  3. Allocation/deployment view

    • Where software runs.
    • Which software runs on which machines.
    • How machines communicate.

A major difficulty is:

Humans are relatively good at reading individual pieces of source code, but much worse at reconstructing the runtime architecture from source code.


Architecturally Evident Coding Style

This is the central idea of the talk.

13. Make the architecture visible in the code

Two programs can behave identically but have very different readability.

The speaker proposes an architecturally evident coding style:

Write code so that important architectural decisions are obvious to another developer reading the code.

This means intentionally doing more than is strictly necessary for the compiler.

For example:

calculateTotalExpenses()

is better for humans than:

te()

Even though both can compile and execute.

The same principle can be applied at the architectural level.


14. Why make architecture evident?

Potential benefits include:

  • Reduce loss of architectural knowledge.
  • Make code easier to understand.
  • Reduce the amount of architecture developers must infer.
  • Improve developer productivity.
  • Make onboarding easier.
  • Reduce documentation burden.
  • Make future architectural violations easier to detect.
  • Make evolution safer.

The central objective is:

Reduce the amount of architecture that developers have to reconstruct from clues.


The Patterns Presented

The speaker presents several techniques for encoding architecture into code.

15. Make module dependencies explicit

If modules have dependency rules:

  • Use tools that can check dependencies.
  • Use module systems/frameworks such as OSGi.
  • Use package structures that communicate intended boundaries.
  • Avoid relying solely on comments.

Why?

  • Comments can become outdated.
  • Comments usually cannot be automatically checked.
  • Machine-checkable dependency rules are stronger.

16. Use package structure to communicate architecture

Package organization itself can communicate design.

For example:

email-system/
    infrastructure/
    components/
    interchange/
    ...

The compiler may not care about this organization.

Developers do.

A good package structure can help reveal:

  • Which code belongs to which architectural area.
  • Where components are located.
  • Which data structures are exchanged.
  • Which architectural style is being used.

17. Align modules with runtime components

There can be a mismatch between:

Source code modules

and:

Runtime components

The speaker recommends making this relationship explicit where practical.

For example:

Module A ─┐
Module B ─┼──> Runtime Component X
Module C ─┘

If the mapping is hidden, developers must reconstruct it mentally.

If the mapping is obvious, the architecture becomes easier to understand.


18. Make runtime instantiation visible

One particularly difficult thing to infer from source code is:

What runtime components actually exist?

A useful pattern is to centralize architecture construction.

For example:

main()
    create components
    create connectors
    connect components
    start components

Rather than scattering initialization across many files.

This makes the runtime topology much easier to see.


19. Separate creation, connection, and startup

For a pipe-and-filter architecture, the speaker recommends a structure roughly like:

createPipes()
createFilters()
connectFilters()
startFilters()

This makes the runtime architecture visible.

Instead of forcing a developer to search through hundreds of lines to discover:

  • What components exist.
  • Which components communicate.
  • How they are connected.
  • When they start.

20. Make components explicit

Components can be made recognizable through:

  • Naming conventions.
  • Marker interfaces.
  • Base classes.
  • Component-specific types.

For example:

abstract class Component {
    ...
}

abstract class Filter extends Component {
    ...
}

Then developers can immediately recognize:

InputCleanupFilter
TaggingFilter
ClassificationFilter

as architectural components.


21. Use types to encode architectural concepts

Instead of representing an architectural concept only through comments, create a type for it.

For example:

class Pipe<T> extends Connector {
    ...
}

Now the architecture is represented directly in the type system.

This provides several benefits:

  • Developers can recognize the architecture.
  • IDE navigation becomes easier.
  • Some mistakes can be detected automatically.
  • Architectural responsibilities can be centralized.

22. Make connectors explicit

Normal code might contain:

A.call(B)

But this does not tell you whether the communication is:

  • Local method call
  • Remote call
  • Event
  • Message
  • Socket
  • Pipe
  • Shared memory
  • Database interaction

The speaker suggests explicitly representing architectural connectors.

For example:

Pipe<Email> pipe;

rather than hiding the communication mechanism inside arbitrary method calls.


23. Put connector responsibilities into connector types

A connector can contain more than just communication logic.

For example:

Connector
   └── Pipe<T>

The connector can enforce:

  • Communication protocols.
  • Error checking.
  • Concurrency behavior.
  • Other architectural rules.

This moves complexity into a reusable architectural abstraction instead of forcing every component to implement it independently.


24. Make ports explicit

The speaker distinguishes:

  • Interface: what operations are available.
  • Port: a runtime interaction point through which a component communicates.

For example, a server may expose the same interface to many clients while maintaining different runtime connection states.

Making ports explicit can help represent:

  • Connection state.
  • Communication protocols.
  • Runtime relationships.

25. Make architectural properties visible

Architectural properties can be expressed through:

Naming

asynchronousWrite()
readOnlyInventory()

Annotations

@Asynchronous
@ReadOnly

Annotations are particularly useful because they can potentially be machine checked.


26. Make architectural styles recognizable

Naming can communicate the architectural style.

For example:

Pipe
Filter

strongly suggests:

Pipe-and-filter architecture

Similarly, names such as:

Facade
Visitor
Adapter

communicate the use of known design patterns.


Hard vs. soft mechanisms

27. Soft mechanisms

Soft mechanisms help humans understand the architecture but are not necessarily machine enforced.

Examples:

  • Naming conventions.
  • Package organization.
  • Comments.
  • Class names.
  • Method names.

Example:

asynchronousWrite()

The compiler does not necessarily verify that the operation is actually asynchronous.


28. Hard mechanisms

Hard mechanisms can be checked or enforced by machines.

Examples:

  • Type systems.
  • Assertions.
  • Preconditions/postconditions.
  • Annotations with validation.
  • Framework constraints.
  • Architectural dependency-checking tools.
  • Marker interfaces.
  • Specialized compilers/preprocessors.

The speaker prefers hard mechanisms when practical because:

A machine-checkable architectural rule is stronger than a rule that exists only in documentation.


Making invariants visible

29. Architectural invariants are particularly difficult

An invariant is something that must always remain true.

Example:

A cache must never be bypassed.

Possible mechanisms:

  1. Encode the invariant in the API.
  2. Use types/classes to enforce it.
  3. Use assertions.
  4. Use modeling/specification languages.
  5. Use automated tests.
  6. Use architectural analysis tools.

The key distinction is that an invariant applies broadly:

"For all components, this rule must remain true."

This is harder to express than simply defining individual components.


Anti-pattern: buried treasure

30. Do not hide unexpected responsibilities

The speaker calls this "buried treasure."

Example:

launchSpaceShuttle()

looks like it launches a space shuttle.

But if it actually calculates a bank balance, the code is misleading.

General rule:

Names should accurately reveal responsibilities.

Do not make developers discover unexpected behavior by digging through the implementation.


The overall philosophy

The talk ultimately argues for this development philosophy:

Architecture/design knowledge
          ↓
     Source code
          ↓
   Make important
 architectural concepts
       visible
          ↓
     Humans can
 understand the
 architecture faster

Rather than:

Architecture
    ↓
Source code
    ↓
????
    ↓
Developer must reverse-engineer
the architecture

The most important takeaway

Don't only write code that works. Write code that makes the important design decisions visible.

That means deliberately using:

  • Meaningful names
  • Good package boundaries
  • Explicit architectural types
  • Explicit components
  • Explicit connectors
  • Explicit ports
  • Centralized runtime assembly
  • Annotations
  • Type-system constraints
  • Dependency-checking tools
  • Tests and assertions

The goal is not to encode every piece of architectural knowledge into source code. The speaker explicitly says that this is impossible.

Instead:

Encode as much important architectural intent as the programming language and tooling can reasonably express, and make the remaining intent as easy as possible for humans to recognize.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment