top of page

Code Rule #02: Annotations Are Not Architecture

  • Writer: Mateusz Roguski
    Mateusz Roguski
  • Apr 18
  • 8 min read

Updated: May 3

A retro-style digital illustration of a clean architectural blueprint labeled “ARCHITECTURE” being overwhelmed by scattered sticky notes and arrows, symbolizing how annotations and metadata can obscure structural clarity — reflecting the article’s message that annotations are not true software architecture.
Convenience is not clarity — and annotations are not architecture.

Table of Contents




Introduction


Annotations (or attributes, depending on your language) have become a widespread tool for reducing boilerplate and wiring up frameworks. They’re everywhere — marking routes, configuring services, mapping database entities, triggering validation, injecting dependencies.

Annotations are not architecture. They’re metadata. Helpful, yes — but they don’t define structure, intent, or clarity. And when overused, they cause more harm than good.


Here are reasons why:


Tight Coupling & Lock-In


Annotations embed framework concerns directly into your code. This ties your application logic to specific technologies and makes it painful to switch frameworks or even upgrade versions. The separation between business logic and infrastructure dissolves.


Reliance on Runtime Reflection


Annotation-driven systems often depend on runtime reflection to interpret behaviour. This means errors appear at runtime instead of compile time, and debugging becomes harder. What could’ve been a compiler-verified design turns into guesswork.


Reduced Testability


Code that relies on annotations is harder to test in isolation. You often need to spin up a full container or runtime context just to activate the behaviour behind an annotation. This slows down tests, introduces global state, and makes mocking complex.


Hidden / Implicit Control Flow


Annotations make things happen without being explicitly called. Logic may execute simply because a class is annotated, making it difficult to trace execution paths. This hidden control flow turns the codebase into a black box.


Architectural Obfuscation


Annotations scatter configuration throughout the code, making it hard to understand how the system is wired together. The high-level design disappears into a sea of metadata. You can’t see the shape of the system by reading the code.


Configuration vs. Implementation Blur


Annotations mix setup with behaviour. It becomes unclear which part of a class is actual logic and which part is just configuration. The architectural drawing gets smudged.


This breaks a fundamental design principle: configuration should be separated from implementation. When these concerns are intertwined, it's harder to reason about, change, or reuse either one independently. Explicit configuration in separate files or bootstrap code makes relationships clearer and encourages better modularity.


Violation of Layered Architecture (esp. in DDD)


When annotations for persistence, serialisation, or validation appear in your domain model, they pull infrastructure concerns into the heart of your business logic. This breaks the independence of the domain layer and violates clean architecture principles.


Diminished Readability & Static Analysis


Annotations hide behaviour from both developers and tools. IDE navigation, static analysis, and "find usage" often fail to reveal what an annotation does or where it takes effect. The result? Code that's harder to understand and maintain.


Harder Debugging & Troubleshooting


You can’t set a breakpoint on an annotation. And when something breaks, it’s usually inside framework internals. You end up sifting through stack traces, hoping to guess which annotation caused the failure.


Steep Learning Curve / Poor Onboarding


New developers can't understand system behavior just by reading the code. They have to learn the framework's implicit rules and the hidden behavior annotations introduce. This slows onboarding and increases the chance of mistakes.


Runtime Performance Overhead


Annotations often require scanning and processing via reflection, especially at startup. In large systems, this creates overhead and longer boot times. While caching helps, it introduces new complexities. Every metadata-driven decision made at runtime is one more thing the compiler can’t help with.


Design Principles Often Violated by Annotations/Attributes


  • Separation of Concerns (SoC): Annotations blur the line between configuration and logic by mixing cross-cutting concerns (e.g. persistence, routing, validation) directly into business classes.

  • Single Responsibility Principle (SRP): A class with annotations for routing, validation, serialisation, etc., is doing multiple jobs — violating the principle of having one reason to change.

  • Dependency Inversion Principle (DIP): Annotations often create implicit dependencies on framework behaviour, violating the intent of depending on abstractions rather than concretion or infrastructure.

  • Explicitness over Implicitness: Annotations favor implicit magic — behaviour is declared but not invoked, breaking the principle that code should clearly show what it does.

  • Encapsulation: Annotations can expose internal logic to external frameworks or bind internals to configuration that lives outside the class’s explicit API.

  • Layered Architecture / Clean Architecture: When domain entities are annotated with ORM, routing, or serialisation details, they become tightly coupled to infrastructure, breaking clean separation between layers.

  • Open/Closed Principle (OCP): Changing behaviour often requires editing annotations inside the class itself, rather than extending behaviour externally, which can limit the class’s extensibility.

  • Tell, Don’t Ask: Annotation-driven frameworks tend to operate with hidden orchestration. The system asks metadata what to do rather than your code telling it what to do explicitly.

  • Modularity / Reusability: Classes annotated for a specific framework are harder to extract and reuse in other contexts, violating principles of modular and portable design.

  • Acyclic Dependencies: Annotations can cause circular references and hidden dependency chains, especially when class discovery is done via scanning or autowiring.


Language and Framework Landscape: Where Annotations (and Their Equivalents) Take Over


Annotations aren't the only way architecture can become implicit. Decorators, macros, DSLs, naming conventions — all of these can behave just like annotations, with the same consequences: hidden control flow, tight coupling, and unpredictable behaviour.

This section explores languages and frameworks that make implicit behaviour easy (and often default), versus those that encourage explicit design and structure.


Languages Prone to Annotation Overuse


These languages either support annotations natively or encourage metadata-driven wiring through decorators or macros. They often rely on runtime reflection, and many popular frameworks built on them default to annotation-heavy design.

Language

Annotation Support

Notes

Java

✅ Native annotations

Spring, Jakarta EE, Hibernate all rely on annotations

C#

✅ Attributes

ASP.NET and Entity Framework embed control via attributes

PHP (8+)

✅ Attributes

Symfony, Doctrine, API Platform use them for routing, DI, ORM

Python

⚠️ Decorators

FastAPI, Django use decorators heavily; behaviour is implicit

TypeScript

⚠️ Decorators (experimental)

Angular, NestJS rely on them for structure

Kotlin

✅ Annotations

Spring Boot + DI frameworks use annotations; reflection heavy

Dart

✅ Metadata annotations

Flutter uses @immutable, @override, and other tags

Ruby

⚠️ No native annotation keyword

DSLs (Rails) replace annotations; implicit wiring through macros

Languages That Favor Explicit Architecture


These languages either lack native annotation support or encourage clarity through code structure, composition, and separation of concerns. Frameworks in these ecosystems tend to wire behaviour through code rather than metadata.

Language

Annotation Support

Notes

Go

❌ None

Explicit, struct-tag-based metadata only for serialization

Rust

⚠️ Compile-time attributes

Macros used for codegen, but logic is explicit and safe

Haskell

❌ None

Pure functional approach with explicit composition

Elixir

❌ None

Uses macros, but system flow remains visible via pipelines

JavaScript

❌ None natively

Frameworks simulate decorators; core language is explicit

Swift

⚠️ Limited

Attributes used for interoperability, not control flow

Frameworks That Rely on Annotations or Annotation-Like Magic


These frameworks make metadata central to how behavior is wired. That metadata may be annotations, attributes, decorators, or DSL-style macros — but the result is the same: behaviour is declared, not invoked.

Framework

Language

Notes

Spring (Boot)

Java

Annotation-driven: routing, DI, lifecycle, AOP

Hibernate

Java

Entity mapping, lifecycle hooks all through annotations

ASP.NET Core

C#

Controllers, validation, routing via attributes

Symfony

PHP

Annotations/attributes used for routing, DI, serialization

Doctrine ORM

PHP

Entities, relationships mapped through attributes

Angular

TypeScript

Components, modules, DI through decorators

NestJS

TypeScript

Heavily decorator-based design model

Flutter

Dart

Behaviour tagged with attributes; state/data linked via metadata

Ruby on Rails

Ruby

DSL macros like has_many, before_save wire behaviour implicitly

Django

Python

Uses decorators and meta-classes to inject routing, validation, ORM logic

These frameworks make it easy to blur configuration, behaviour, and structure — often at the expense of clarity and separation of concerns.


Frameworks That Support Explicit Wiring


These frameworks support annotations or decorators but give you the option to build without them — through config files, explicit code, service definitions, or constructor injection. They enable metadata-free architecture when you choose to prioritise structure.

Framework

Language

Annotation-Free Options

Symfony

PHP

YAML/XML for routing, DI, serialization

Laravel

PHP

Explicit service container, fluent routing, no annotation dependency

ASP.NET Core

C#

Middleware pipelines and manual registration fully supported

Micronaut

Java

Compile-time DI, minimal reflection use

FastAPI

Python

Decorators used, but code remains readable and explicit

Express.js

JavaScript

Middleware and routing explicitly wired through code

Go Fiber / Echo

Go

All behaviour declared manually — no annotations

Elixir Phoenix

Elixir

Uses controller functions and pipelines — no metadata wiring

Vapor

Swift

Explicit route declarations, DI via code

Hanami (Ruby)

Ruby

Emphasizes explicit actions, clean architecture, minimal DSL use

In these frameworks, architecture remains visible — not hidden behind metadata or naming conventions.


Annotations aren’t the only threat to architectural clarity. Any system that encourages implicit behavior — whether via metadata, decorators, or DSLs — should be used with caution. The more magic your framework does for you, the more deliberate your structure must be.


What to Do Instead


Instead of relying on annotations to wire your system, structure it explicitly. Here are better practices to guide your design:

  • Use explicit wiring — prefer declarative configuration or composition over annotation-based auto-wiring.

  • Define wiring (like dependency injection, routing, etc.) in the application or infrastructure layers — not inside your business logic.

  • Separate configuration from implementation — keep setup in dedicated config files or bootstrap code.

  • Compose behaviour instead of decorating it — use functions, handlers, or pipelines instead of metadata.

  • Design behaviour to be statically visible — structure code so flow and relationships are obvious.

  • Keep your core pure — avoid annotating domain models or business rules; isolate framework dependencies to adapters.

  • Organize by architecture, not annotation — follow DDD or layered principles with clear folders and module boundaries.

  • Use frameworks as tools — bend them to your system’s needs, not the other way around.

Architecture isn’t how little you write — it’s how clearly your system communicates its intent.


Choose clarity. Choose structure.


Design Principles This Approach Promotes


  • Separation of Concerns — configuration lives outside the logic; layers remain independent.

  • Single Responsibility Principle — each class does one thing, and only one.

  • Explicitness Over Implicitness — behavior is defined in visible, navigable code.

  • Encapsulation — internal logic isn’t exposed to framework mechanisms.

  • Layered Architecture / Clean Architecture — domain remains isolated from infrastructure.

  • Open/Closed Principle — behavior can be extended without modifying the core.

  • Tell, Don’t Ask — your code drives the system, not metadata scanning.

  • Modularity & Reusability — logic is portable across contexts and frameworks.

  • Acyclic Dependencies — dependencies are defined explicitly, not discovered through scanning.

  • Testability — classes are easily testable in isolation without framework bootstrapping.


Impact on AI-Driven Development


In an era where AI tools help us understand, navigate, and even generate code, clarity and structure have never been more important.


  • Annotations hide meaning — AI models rely on explicit syntax and structure to understand intent. Metadata-driven behavior is often invisible to them.

  • Implied logic breaks inference — AI can’t follow control flow that isn’t expressed as code. Hidden behaviour causes hallucinations or missed connections.

  • Opaque systems confuse tooling — Code completion, refactoring, or explanation tools work best with visible logic and contracts, not metadata conventions.

  • Implicit contracts reduce reliability — If AI can’t see how your system works, it can’t safely modify or extend it.

By writing code that favours clear boundaries, explicit control flow, and modular structure, you’re not only helping humans — you’re helping machines help you.


Good architecture is machine-readable architecture.

And annotations, by nature, aren’t.

Summary: Why "Annotations Are Not Architecture"


Annotations are useful — but they are not architectural tools. They offer convenience at the cost of clarity, predictability, and maintainability. When overused, they hide behaviour, break encapsulation, and erode architectural boundaries.


Instead, prefer explicit design, visible wiring, and strict separation of concerns. This leads to systems that are not only easier to reason about and test, but also more readable by both humans and machines.


Annotations can help — but they must never define the system.

Because in well-architected software, behaviour is intentional, not inferred.




Sign up to get the latest coding tips and architecture insights.

  • LinkedIn
  • X

Code, architecture, and the future of software — one post at a time.

Contact me: 

bottom of page