Skip to content

Knowledge base

Architecture Principles

Architecture principles turn engineering values into decisions a team can check: align systems with business domains, communicate without blocking, keep services small, deliver continuously, couple through proven low-tech means, decide in public, and assume attack. Each principle carries a statement, a rationale, implications and review questions.

Written by

Updated:

Software Engineering 5

Our position. We hold that the codebase is the contract the agent works inside. A clean codebase with tests, imitable naming, loud architecture boundaries and fitness functions decides who gets value from AI, more than the model or the token spend (the codebase is the contract (opens in a new tab)). Codebases turn into a mess when nobody codified the architecture decisions, so teach the agent your architectural patterns.

The eleven principles below derive from the five values in the Engineering Manifesto, and both come from OTTO’s public Tech Manifest (opens in a new tab) (Apache-2.0). Written down, they are also the boundaries an agent can read and a review can check.

Apply them with common sense. Deviations happen; they should be the exception and they should be discussed openly with the teams they affect.

Where should system boundaries run, and how do systems talk?

Bounded Contexts and Vertical Systems

Statement: IT systems should be built for business needs, so we align IT products with business domains using bounded contexts and domain models that reflect the business language.

Rationale: Full ownership and clear responsibility is one of the most important aspects if teams should be able to act independently of each other. We identify bounded contexts following the Domain Driven Design approach. Ideally, nothing should be shared between identified contexts, especially not the domain model. This leads to IT products that can be assigned to one bounded context and owned by one team. Each IT product includes everything to fulfill the business need: data, business logic, and UI fragments.

Implications:

  • Every identified context has its own domain model, including owning its own data, business logic, and UI
  • Communication between contexts is designed using domain events
  • Use the domain language (ubiquitous language) in software artifacts. Avoid anemic domain models
  • To find the boundaries, walk the domain story’s events in order: where the aggregate changes or the acting role changes, a context boundary is likely. A visual glossary drawn during the Domain Storytelling workshop is the earliest form of the ubiquitous language, valid per context rather than company-wide. On the resulting context map, draw synchronous exchange as a solid arrow labelled with the aggregate and asynchronous exchange as a dashed arrow labelled with the event and the aggregate it carries; that picture plus an OpenAPI or AsyncAPI skeleton is enough for a model to draft the first API, with descriptions, examples, and security added by hand (codecentric - Der Softwerker Vol. 24: Von der Domain Story zum Interface (opens in a new tab), German)
  • For UIs we prefer composition using micro-frontend patterns over a frontend/UI monolith

Discussion Points:

  • Is the boundary of your context clear?
  • Can you name your boundary events?
  • How is the relationship to your upstream and downstream contexts?
  • Do you own your context fully?
  • Do you have a clear understanding of your domain model and is it shared with the business side to establish a ubiquitous language?

Motivating values: Full ownership and clear responsibility, Tech follows Business

Non-Blocking Communication

Statement: We prefer non-blocking technical communication between systems to achieve technical autonomy from other systems.

Rationale: Blocking calls to remote systems can lead to long-running threads waiting for outside dependencies. This can completely paralyze a system as more and more threads move into that state until the system can no longer react to new requests. If multiple services are chained, aside from the increased latency, this can lead to cascading errors affecting all involved systems. Recovery might also be affected, as restarted services might immediately get heavy traffic, bringing them down again. Technical communication should follow a standard non-blocking pattern such as pubsub, feeds, or webhooks.

Implications:

  • We reduce the impact of remote failures by communicating non-blocking via events where possible. Services publish streams of events, which interested services consume asynchronously
  • Communication with other systems becomes non-blocking: even when some functionality is temporarily affected, the system continues working
  • Where necessary, events should contain additional ordering information
  • Implement services as an idempotent receiver (opens in a new tab) where possible
  • If a synchronous call is necessary, use circuit breakers, timeouts, and bulkheads (see service degradation)
  • A chain of synchronous integration calls across several layers adds each hop’s latency and unreliability: tolerable inside a batch job, usually a mistake behind a user interface (codecentric - Der Softwerker Vol. 20: Integrationsprobleme in der Schokoladenzentrale (opens in a new tab), German)

Discussion Points:

  • What is the right way to model events? Event carried state transfer (which includes the entire state), event notification, command messages, or only deltas published?
  • Are you publishing your transaction log as events or are your events tailored to actual business events and the respective consumers? Avoid puncturing encapsulation with change data capture (opens in a new tab) between business contexts
  • Are the events idempotent? Is there a need to make ordering an explicit property of the events?
  • Does the event inform about a state change or is it actually a command (customer email updated vs. publish refund notification)?
  • Is the event catering to multiple consumers and generic (changes are harder) or tailored for a specific consumer (change affects only one system)?
  • Do the events contain GDPR/PII information? Are all consumers allowed to have access to them?

Motivating values: Coverage of non-functional aspects, Full ownership and clear responsibility

How big should a service be, and how does it evolve?

Small and Simple (Microservices)

Statement: Build services small and simple. Design them around business capabilities, based on a domain model with state and behavior.

Rationale: A service should be big enough to offer a valid business capability, but small enough to be handled by a two-pizza sized team. In practice, a team may be able to own and run a large number of small services, or a smaller number of larger services. Building small and simple makes systems easier to understand. But with more systems the interactions between them can become more complex. The goal is not to design as many little microservices as possible, but to design a system landscape within a team’s domain where the different subdomains are implemented as independently as possible. Subdomain-overarching microservices should be avoided.

Implications:

  • Prefer microservices over monoliths
  • Ensure we leverage the benefits and make them independently deployable, avoiding a distributed monolith
  • Model microservices based on your domain (a domain can have multiple microservices)
  • Consider using Anti-Corruption Layers to decouple services from surrounding domains
  • Microservices should be independently testable
  • Take the 12 factor app (opens in a new tab) into consideration when designing microservices

Discussion Points:

  • Are your services independently deployable and testable?
  • Are you building a distributed monolith?
  • Are your microservices sliced based on your domain or is the slicing of a rather technical nature?
  • Are you building micro- or nano-services?
  • Do your microservices cater to multiple domains?
  • Are your microservices actually independent or are they integrating on the database layer?
  • Are you tall enough to use microservices? (opens in a new tab)

Motivating values: Full ownership and clear responsibility, Move fast fail fast, Low technical barriers

Evolutionary Architecture

Statement: Systems and architectures should be designed and built to evolve independently of other systems.

Rationale: At any given point in time we know less than what we will know in six months. Our software should be able to evolve as we learn more, both at a code level and at a service level within the business responsibilities.

Implications: - Defer accidental complexity as much as possible. Focus on addressing current needs, accept that we are poor at predicting future needs - Systems and processes will need to be exposed through clearly defined APIs so that the underlying implementation can change and evolve without impacting the wider system. (This does not mean that a component’s internal APIs necessarily need the same treatment.) - Consider patterns such as TolerantReader (opens in a new tab), backwards compatibility, and the Principle of Robustness (opens in a new tab), if applicable or useful - Standards derived from working software are preferred over standards derived from working groups - Sometimes gradual change is insufficient as requirements shift, so consider a Sacrificial Architecture (opens in a new tab) as a pattern. Do not be afraid to throw away and rewrite; this is often faster and leads to better quality.

If we have kept things “Small and Simple” this should not become a painful Big Rewrite - Consider the use of standards (software and protocols). If they do not reasonably fit your needs, evaluate highly adopted alternatives. Industry standards over company standards to cover the needs of external clients

Discussion Points:

  • Are you implementing actual or anticipated requirements?
  • Did you design using a Big Design Up Front approach?
  • Do your service interfaces and protocols rely on well-known standards or are they tightly coupled to a proprietary third-party tool/protocol?
  • Does your software rely on well-known standards or are they tightly coupled to a proprietary third-party tool/protocol?

Motivating values: Move fast fail fast, Low technical barriers

How should systems run in the cloud?

Cloud Native

Statement: We prefer services directly provided by cloud platforms like AWS or GCP. Teams choose the provider that fits best, but should always be prepared to explain why a specific provider was selected.

Rationale: We want to fully leverage the cloud and use platform-provided services wherever possible. This limits the technical landscape and removes the burden of operating and scaling backing services. Team-specific vendor lock-in is accepted: teams use high-level services like databases or serverless infrastructure that may not exist equally at other providers. Still, teams should follow good development principles like decoupling business logic from infrastructure components.

Implications: The overall priority for selecting infrastructure and supporting services:

  1. Prefer cloud services provided by the team’s primary cloud provider
  2. If nothing fits, use third-party Software-as-a-Service offerings
  3. Only if nothing fulfills requirements, build or operate your own service within your cloud account:
    • Self-hosting an active open source solution
    • Self-hosting a third-party paid solution
    • Self-implementing and hosting a solution

Discussion Points:

  • Which cloud provider offers the best technical fit for our software?
  • How have other teams solved that particular problem?
  • Is the chosen technology really necessary?
  • Is a support level appropriate or needed for a selected component?

Motivating values: Move fast fail fast, Low technical barriers

Scale Horizontally

Statement: System components should be horizontally scalable where possible.

Rationale: It is cheaper to scale services by adding inexpensive resources like commodity servers or database nodes rather than buying larger and larger pieces of hardware which at a given point are unable to scale further. By designing systems for horizontal scalability from the start we eliminate the cost of changing later.

Implications:

  • State data cannot be saved in application memory. Consider the use of shared databases or in-memory data grids like memcached or Redis
  • Services should strive to be idempotent

Discussion Points:

  • Is ACID functionality required or is eventual consistency (BASE) sufficient?

Motivating values: Move fast fail fast

How do you ship changes safely and often?

Continuous Delivery and Deployment

Statement: Strive for short release cycles and a limited number of changes per release; automating the delivery pipeline makes this possible.

Rationale: Small releases tend to have fewer bugs. Continuous Integration usually refers to integrating, building, and testing code within the development environment. Continuous Delivery and Deployment builds on top of this, dealing with the final stages required for production deployment. This reduces deployment risk, shows believable progress, and reduces the time to get feedback from real users.

Implications:

  • Use feature toggles: not only to separate deployment from release and let business decide when to turn on a feature, but also as a mechanism to quickly turn off broken features and limit their impact. Maintain different feature toggle sets across different stages/environments
  • A release-toggle mechanism earns its keep on four requirements: adding a toggle touches few places, removing one is a full-text search and a deletion with no new code, each environment activates it independently, and flipping it needs no commit or rebuild. Keep the toggles in the application’s own startup configuration rather than in an external toggle service, because a service that is unreachable forces a default nobody can state correctly (which of two providers is the right fallback?). In the test setup, enable every toggle by default: the tests a new feature breaks fail the day the toggle goes in, and removing the toggle later means deleting the toggle-off cases rather than rereading the suite (codecentric - Der Softwerker Vol. 20: Jederzeit deployen können (opens in a new tab), 2022, German)
  • Use canary testing for new deployments to identify problems early
  • Have an automated test suite with high coverage to support rapid deployments
  • Use Consumer Driven Contract Tests to ensure changes are not breaking dependent systems
  • Consider push-button deployments of any version of the software to any environment on demand, especially for rapid software rollbacks
  • Use a single trunk-based CI build as the beginning of the pipeline

Discussion Points:

  • What manual gates are in place? Is it useful to have one?
  • Can we release straight to production?
  • Is a green build trustworthy for all team members? If not: what steps would achieve this?
  • Which trust-building actions (tests, CDCs, and so on) need to be implemented to achieve sufficient trust?

Motivating values: Low technical barriers, Move fast fail fast

Which technologies should teams choose?

Sensible Defaults

Statement: We minimize technology variation and instead leverage sensible defaults across teams.

Rationale: We deliberately minimize the variety of technologies in a system to drive efficiency, and at the same time maintain a managed program of innovation. To achieve this, we monitor actual technologies in use, gravitate to those most appropriately addressing the given challenges, and selectively experiment with new technologies to find those that will improve products. Maintaining a technology radar provides a good overview of current technology selection and usage. In addition, skeleton templates help quickstart newly formed teams. The radar is a guideline recommendation. Usage of technologies not listed is not prohibited, but teams need to reason why they have chosen a specific technology.

Implications:

  • Use the technology radar for technology selections
  • Discuss the need for new technologies with others
  • Regularly contribute to and challenge the radar

Discussion Points:

  • How often should the technology radar be updated?
  • What are the criteria for the rings (trial, assess, adopt, hold) on the tech radar?

Motivating values: Low technical barriers, Tech follows Business

Use Low-Tech Coupling

Statement: We prefer simple, proven technologies for communication. Smarts in the nodes, not the network.

Rationale: Low-tech coupling reduces issues resulting from changes in communicating systems, and can reduce complexity and dependencies. Reduced complexity makes it easier to locate and identify problems. It also makes it easier for new teams to participate and, for example, tap into event streams. Proven, simple solutions reduce the cognitive load when talking about service-to-service communication.

Implications:

  • Use DNS for service discovery
  • Use interoperable protocols like HTTP for synchronous communication instead of, for example, RMI
  • Prefer human-readable formats like JSON for payloads (instead of protobuf or msgpack)
  • Prefer event streaming from the data master directly instead of using integration patterns like Enterprise Service Bus
  • Agreeing on JSON, XML, or a spreadsheet is not a specification; the meaning of each field still has to be settled, and integration projects rarely fail on the technology. Between m sources and n consumers, either accept m×n point-to-point routes or put one hub in the middle for m+n, and the craft is to create no second hub and to design the hub’s data model deliberately. Where no system leads with a foreign-key column for the others’ identifiers, the integration needs its own store for the mapping (codecentric - Der Softwerker Vol. 20: Integrationsprobleme in der Schokoladenzentrale (opens in a new tab), 2022, German)

Discussion Points:

  • What kind of problems would a more complex solution fix? Do we actually have that problem?
  • Do we observe challenges because of an in-use low-tech solution across the organization?
  • Is the performance gained by more complex solutions necessary in the particular context where it would be used?

Motivating values: Low technical barriers, Coverage of non-functional aspects

How should cross-team decisions be made?

Make Decisions in Public

Statement: We prefer to make cross-team decisions in public, ask for feedback, and document the conclusion.

Rationale: A lot of technical and design decisions affect more than one team or even the entire engineering organization. We want to leverage combined knowledge and make decisions in public, allowing everyone interested to participate and come to the best possible conclusion. This does not mean decisions need to be majority votes, but that everybody is given the chance to provide feedback and arguments. Often, some time after a decision has been made, especially if it causes unanticipated challenges, it helps enormously to understand the context, knowledge, and anticipated consequences at the point in time the decision was made.

Implications:

  • We value an RFC (Request for Comment) approach on a cross-team level where possible. The RFC should contain the challenge, possible solutions, and at some point the decision made and why it was made
  • We keep RFCs at a central place for easy lookup
  • Within a team, we recommend leveraging ADRs (Architecture Decision Records) (opens in a new tab) to document major architectural or technical decisions

Discussion Points:

  • Has there been enough time to actually provide comments to an RFC?
  • Is the decision properly documented at an easy-to-find location?
  • Has your team documented important decisions in the form of an ADR?

Motivating values: Low technical barriers, Tech follows Business

How much security does a system need?

Security First

Statement: Systems will be designed and maintained with the assumption that they will be attacked and possibly compromised by anybody.

Rationale: The cost of handling a security breach is significantly greater than the cost of hardening the system in the first place. If a breach does occur, we can minimize the scale and cost by detecting it as soon as possible. A well-planned response enables a fast time to close the breach and assess its impact. By focusing on security from the beginning, we mitigate not only the financial impact but also the reputational impact that would be incurred in the event of a breach.

Implications:

  • Follow a security application checklist (Security Dojo approach) and participate in security guilds
  • Have a security champion/catalyst and consider the security aspects of every story
  • Always use TLS and make sure the caller of your service is authenticated and authorized. HTTPS everywhere, not HTTP
  • Use threat modelling workshops to identify threats inherent to the domain (e.g. amplification attacks where a small number of malicious database queries causes a service outage)
  • Design services and implementations in a secure-by-default manner (e.g. a service by default enables TLS and authorization/authentication instead of explicitly activating it)
  • Have monitoring in place to automatically detect brute force attacks, unusual requests, or unprotected resources

Discussion Points:

  • Is all your traffic encrypted?
  • Are all endpoints protected by access control?
  • Are your backups encrypted (and signed)?
  • Would you know if your cloud costs are unusually high?

Motivating values: Coverage of non-functional aspects, Tech follows Business, Full ownership and clear responsibility

Resources

Engineering practice