Skip to content

Instantly share code, notes, and snippets.

@MangaD
Created July 15, 2026 00:56
Show Gist options
  • Select an option

  • Save MangaD/e9ff752706b5d69d942f4e9324e1ef8b to your computer and use it in GitHub Desktop.

Select an option

Save MangaD/e9ff752706b5d69d942f4e9324e1ef8b to your computer and use it in GitHub Desktop.
Systems Thinking and Systems Programming

Systems Thinking and Systems Programming

CC0

Disclaimer: ChatGPT generated document.

Understanding Software from the Whole System Down to the Machine

Modern software engineering operates across an extraordinary range of abstraction.

At one end, engineers reason about business goals, user behavior, organizational structure, security policy, reliability, regulation, and long-term maintenance. At the other end, they reason about machine instructions, memory layouts, system calls, processor caches, network packets, synchronization primitives, and hardware failures.

Two disciplines are especially important across this range:

  • systems thinking
  • systems programming

The terms sound similar, but they describe different things.

Systems thinking is a way of reasoning. It is the practice of understanding software as part of a larger, interconnected, evolving system.

Systems programming is a technical discipline. It concerns the construction of foundational software that interacts closely with operating systems, hardware, runtime environments, networks, storage devices, and other low-level resources.

The two disciplines are distinct, but they strongly complement each other.

A systems programmer without systems thinking may produce efficient low-level components that are difficult to operate, integrate, observe, secure, or maintain.

A systems thinker without systems-programming knowledge may understand the architecture conceptually but lack the technical depth required to predict how the operating system, memory hierarchy, network stack, compiler, or hardware will affect the system in practice.

The strongest engineers are often able to move between both perspectives. They can inspect a single allocation, system call, lock, packet, or cache miss while still understanding how that detail influences reliability, user experience, organizational cost, and the evolution of the larger system.

This article examines both systems thinking and systems programming in depth. It explains their foundations, their differences, their relationship, and their importance in modern software engineering.


Part I: What Is a System?

Before discussing systems thinking, it is necessary to define a system.

A system can be described as:

A collection of interacting elements whose relationships produce behavior over time.

This definition contains several important ideas.

A system has:

  1. components
  2. relationships
  3. boundaries
  4. inputs
  5. outputs
  6. state
  7. behavior
  8. goals or functions
  9. an environment
  10. change over time

A system is therefore more than a collection of parts.

A pile of server components is not necessarily a working computer. A directory full of source files is not necessarily a coherent software system. A set of independently correct microservices is not necessarily a reliable distributed application.

The parts must interact.

Those interactions determine the behavior of the whole.

Components and relationships

Consider a simple online store.

Its components might include:

  • a web frontend
  • an API
  • a user database
  • a product database
  • a shopping cart service
  • a payment provider
  • an inventory service
  • an email service
  • an analytics platform
  • a deployment pipeline
  • a monitoring system

A reductionist view studies each component separately.

A systems view studies both the components and the relationships between them.

For example:

  • How does a payment timeout affect inventory reservation?
  • What happens if the email service fails after payment succeeds?
  • Can a user submit the same order twice?
  • What happens if the product database is updated while an order is being processed?
  • How do retries interact with payment operations?
  • How is an incomplete transaction detected and repaired?

The most difficult problems are often found in the relationships.

State

Systems have state.

State is the information that influences future behavior.

Examples include:

  • the contents of memory
  • the rows in a database
  • the status of an order
  • the ownership of a lock
  • the sequence number of a network packet
  • the current deployment version
  • the number of failed login attempts
  • the remaining disk space
  • the number of active connections

State makes systems more difficult to reason about because the same input may produce different outputs depending on what happened previously.

A stateless mathematical function is generally easier to understand than a distributed transaction involving caches, queues, retries, databases, and external services.

Boundaries

Every system model has a boundary.

The boundary determines what is considered part of the system and what is treated as the environment.

For one engineer, the system might be a single function.

For another, it might be a library.

For another, it might be an entire cloud platform.

For a product manager, the system might include users, customer support, pricing, legal requirements, and business strategy.

No boundary is universally correct.

The useful boundary depends on the question being asked.

For example, if a socket connection is failing, a programmer might initially define the system as:

  • application
  • socket library
  • operating-system networking API

If the problem remains unexplained, the boundary may need to expand to include:

  • DNS
  • firewalls
  • routers
  • proxies
  • load balancers
  • certificate authorities
  • server configuration
  • cloud networking
  • third-party infrastructure

Systems thinking frequently requires changing the boundary until the important causes become visible.

Purpose and function

Some systems are deliberately designed for a goal.

A compiler translates programs.

A database stores and retrieves data.

An operating system manages hardware resources.

Other systems acquire behavior that was not explicitly designed.

For example, an organization may develop an informal process in which engineers avoid deploying on Fridays because previous incidents created fear and caution. That behavior may never appear in an official document, but it becomes part of the real software-delivery system.

The actual function of a system is not always identical to its stated purpose.

A ticketing process may claim to improve organization but may actually discourage small improvements by adding excessive administrative cost.

A review process may claim to improve quality but may primarily centralize authority.

A microservice architecture may claim to support team autonomy but may create more cross-team coordination because services are poorly separated.

Systems thinking examines what a system actually does, not merely what its designers say it does.


Part II: What Is Systems Thinking?

Systems thinking is the practice of understanding phenomena through relationships, structures, feedback loops, delays, constraints, and behavior over time.

In software, systems thinking means treating software not merely as code, but as part of a broader socio-technical environment.

This environment may include:

  • source code
  • hardware
  • operating systems
  • networks
  • databases
  • cloud providers
  • deployment systems
  • users
  • engineers
  • managers
  • support teams
  • organizational incentives
  • regulations
  • budgets
  • external dependencies
  • attackers
  • competitors
  • maintenance processes
  • business objectives

Systems thinking asks how these parts influence one another.

It rejects the assumption that a problem can always be understood by isolating a single component.

A shift in questions

A conventional local question might be:

How can this function be made faster?

A systems-thinking question is:

Does making this function faster improve the performance of the complete user request?

A local question might be:

Why did this service return an error?

A systems-thinking question is:

What combination of load, retries, dependency failures, deployment changes, queue growth, and user behavior caused this failure to occur?

A local question might be:

Should we introduce a microservice?

A systems-thinking question is:

How would a new service affect deployment, ownership, latency, consistency, observability, testing, team communication, operational cost, and long-term evolution?

Systems thinking changes not only the answers but also the questions.


Part III: Reductionism and Holism

Reductionism is the practice of understanding a complex object by decomposing it into smaller parts.

It is one of the foundations of science, mathematics, and software engineering.

Software developers use reductionism constantly:

  • decomposing programs into modules
  • decomposing algorithms into functions
  • decomposing systems into services
  • decomposing requirements into tasks
  • decomposing problems into smaller problems

Reductionism is not wrong.

It is essential.

The problem occurs when decomposition causes the relationships between parts to disappear from view.

The limits of isolated reasoning

Suppose every service in a distributed system is individually correct.

Each service:

  • passes its unit tests
  • handles valid inputs
  • returns correct outputs
  • uses memory safely
  • meets its local latency target

The overall system may still fail because of:

  • incompatible timeout policies
  • circular retries
  • inconsistent assumptions
  • duplicate message delivery
  • clock differences
  • queue accumulation
  • connection exhaustion
  • cascading failures
  • partial deployments
  • schema mismatches
  • race conditions across service boundaries

No single component contains the entire failure.

The failure exists in the interaction.

This is one of the central lessons of systems thinking:

A system cannot always be understood by examining its components independently.

Holism

Holism emphasizes the behavior of the whole.

A holistic view asks:

  • How do the parts interact?
  • What behavior emerges from those interactions?
  • How does the environment influence the system?
  • How does the system adapt?
  • What happens over time?
  • Which structures produce recurring patterns?

Software engineering requires both reductionism and holism.

Reductionism helps engineers implement components.

Holism helps engineers understand the system those components create.


Part IV: Software as a Socio-Technical System

Software is often described as a technical artifact, but real software systems are socio-technical.

They combine technology and human organization.

A production system includes not only code and infrastructure but also:

  • who can deploy
  • who owns each service
  • how incidents are reported
  • how decisions are approved
  • how requirements are communicated
  • how teams share knowledge
  • how engineers are rewarded
  • how risks are evaluated
  • how customers respond
  • how support teams escalate problems

These human and organizational elements are not external distractions. They directly influence technical behavior.

Organizational structure and architecture

An organization divided into isolated departments may produce software with rigid boundaries and slow integration.

An organization with unclear ownership may produce:

  • abandoned services
  • duplicated functionality
  • inconsistent APIs
  • unresolved incidents
  • outdated dependencies

An organization that rewards feature delivery but ignores maintenance may accumulate technical debt.

An organization that punishes failure harshly may encourage employees to hide problems.

An organization that measures engineers by lines of code may unintentionally reward unnecessary complexity.

The architecture of software is affected by the architecture of the organization.

Conway's Law

Conway's Law is commonly summarized as the observation that organizations tend to design systems that reflect their communication structures.

For example, if four teams build a compiler and communicate poorly, the result may resemble four loosely connected compiler stages with awkward interfaces.

If frontend and backend teams are separated by organizational barriers, the API between them may become slow to evolve.

If operations and development are treated as unrelated groups, production concerns may be discovered only after deployment.

Systems thinking therefore considers team structure part of technical architecture.

Incentives

People respond to incentives.

Suppose a team is measured only by deployment frequency.

It may deploy frequently while:

  • ignoring reliability
  • splitting changes artificially
  • increasing operational burden
  • avoiding difficult maintenance work

Suppose a support team is measured only by ticket closure rate.

It may close tickets quickly without solving recurring root causes.

Metrics influence behavior.

A metric is not merely an observation. Once tied to incentives, it becomes part of the system.


Part V: Interconnections and Dependencies

Modern software contains enormous dependency networks.

A single application may depend on:

  • a programming language
  • a compiler
  • a standard library
  • an operating system
  • shared libraries
  • package repositories
  • open-source projects
  • cloud services
  • DNS
  • certificate authorities
  • identity providers
  • payment processors
  • monitoring vendors
  • content-delivery networks
  • databases
  • message brokers

Each dependency adds capability, but it also adds risk.

Direct and indirect dependencies

A direct dependency is one that the application explicitly uses.

An indirect dependency is introduced by another dependency.

For example:

Application
    |
    v
HTTP framework
    |
    v
TLS library
    |
    v
Cryptographic library

The application may never directly call the cryptographic library, but its security depends on it.

Dependencies create:

  • upgrade obligations
  • security exposure
  • compatibility risks
  • licensing concerns
  • operational requirements
  • performance overhead
  • failure modes

A systems thinker evaluates dependencies as part of the architecture.

Hidden coupling

Two components can be coupled even when they do not directly call one another.

They may be coupled through:

  • a shared database
  • a message schema
  • a file format
  • a configuration convention
  • a deployment order
  • an operational process
  • shared resource limits
  • a common identity provider
  • timing assumptions

For example, two services may appear independent but use the same database connection pool. Heavy load in one service may starve the other.

This is resource coupling.

Two teams may use the same schema but update it independently. This is data coupling.

A deployment may require one service to be upgraded before another. This is temporal coupling.

Systems thinking looks for these less visible forms of dependency.


Part VI: Feedback Loops

Feedback occurs when the output of a system influences its future input.

Feedback loops are fundamental to the behavior of complex systems.

There are two broad categories:

  • reinforcing loops
  • balancing loops

Reinforcing feedback loops

A reinforcing loop amplifies change.

Consider a successful product:

More users
    ↓
More usage data
    ↓
Better recommendations
    ↓
Better user experience
    ↓
More users

This loop supports growth.

Reinforcing loops can also produce failure.

Higher latency
    ↓
Client timeouts
    ↓
Client retries
    ↓
More server load
    ↓
Higher latency

The retry mechanism was intended to improve reliability.

Under overload, it reduces reliability.

This is a classic systems effect: a locally reasonable mechanism produces globally harmful behavior.

Retry storms

Suppose a service handles 10,000 requests per second.

A dependency becomes slow.

Clients retry each failed request three times.

The dependency may now receive close to 40,000 attempts per second instead of 10,000.

The additional load makes recovery less likely.

Possible protections include:

  • exponential backoff
  • random jitter
  • retry budgets
  • circuit breakers
  • rate limiting
  • load shedding
  • bounded queues
  • idempotency
  • deadlines

These are not merely programming techniques. They are interventions in the system's feedback structure.

Balancing feedback loops

A balancing loop resists change and moves the system toward stability.

For example:

CPU utilization rises
    ↓
Autoscaler adds instances
    ↓
Load per instance falls
    ↓
CPU utilization falls

Other balancing mechanisms include:

  • rate limiting
  • congestion control
  • admission control
  • queue limits
  • circuit breakers
  • thermal throttling
  • flow control
  • garbage collection
  • memory pressure handling

A stable software system usually contains many balancing loops.

Delayed feedback

Feedback can be delayed.

Delays make systems harder to manage because the effects of actions may appear much later.

Examples include:

  • technical debt
  • memory leaks
  • slow database growth
  • customer churn
  • security exposure
  • dependency decay
  • operational burnout
  • gradual performance regression

A team may accelerate delivery by reducing testing. The short-term result looks positive. Months later, defects, fear of deployment, and maintenance cost increase.

The delay hides the causal relationship.

Systems thinkers pay close attention to delayed consequences.


Part VII: Emergence

Emergence occurs when interactions between components create system-level behavior that is not obvious from the individual parts.

Examples in software

Emergent behavior includes:

  • deadlocks
  • race conditions
  • congestion collapse
  • cache stampedes
  • distributed inconsistency
  • traffic spikes
  • social-media virality
  • attack amplification
  • market effects
  • unexpected user workflows
  • performance cliffs

A thread is not a deadlock.

A lock is not a deadlock.

A deadlock emerges from a cycle of resource ownership and waiting.

A cache node is not a cache stampede.

A cache miss is not a cache stampede.

The stampede emerges when many clients simultaneously miss the same entry and overload the backing service.

Nonlinearity

Complex systems are often nonlinear.

A small input does not always produce a small output.

Examples:

  • one missing database index causes a query to become hundreds of times slower
  • a slight load increase crosses a queueing threshold and creates massive latency
  • one certificate expiration breaks communication across many services
  • one malformed message blocks an entire consumer partition
  • one lock-order inversion freezes multiple threads

This means that proportional intuition is often unreliable.

A system may appear healthy until it crosses a threshold.

Then behavior changes dramatically.


Part VIII: Stocks, Flows, Queues, and Accumulation

A useful systems-thinking model distinguishes between stocks and flows.

A stock is something that accumulates.

A flow changes the stock.

Examples in software include:

Stock Inflow Outflow
Request queue Incoming requests Processed requests
Database size New records Deletion or archival
Technical debt Shortcuts and deferred work Refactoring and cleanup
Open incidents New failures Incident resolution
Memory usage Allocations Deallocations
Connection pool usage Acquired connections Released connections

Queue accumulation

Suppose requests arrive at 1,000 per second, but the service can process only 900 per second.

The queue grows by 100 requests every second.

Initially, the system may appear functional.

After one minute, 6,000 requests are waiting.

Latency rises continuously.

Eventually:

  • timeouts increase
  • retries begin
  • memory usage grows
  • downstream services receive bursts
  • the process may crash

The problem is not only that processing is slow.

The deeper problem is that inflow exceeds outflow.

Technical debt as a stock

Technical debt also accumulates.

It increases through:

  • rushed implementation
  • missing tests
  • outdated dependencies
  • unclear ownership
  • duplicated logic
  • weak documentation
  • incomplete migrations
  • temporary workarounds

It decreases through:

  • refactoring
  • dependency upgrades
  • documentation
  • redesign
  • deletion
  • automation
  • improved testing

Teams often focus on the inflow of features while ignoring the stock of complexity.

Eventually, change becomes slow and risky.


Part IX: Bottlenecks and Constraints

A system's throughput is often limited by its most restrictive constraint.

Optimizing non-constraints may produce little or no improvement.

A simple performance example

Consider a request with the following latency:

Parsing:             4 ms
Authentication:      6 ms
Database query:    700 ms
Serialization:       3 ms
Network transfer:   12 ms

If parsing is made twice as fast, the request saves 2 milliseconds.

If the database query is improved by 30 percent, the request saves 210 milliseconds.

The correct optimization target depends on the whole path.

The shifting bottleneck

Once one bottleneck is removed, another becomes visible.

For example:

  1. The database is optimized.
  2. The application server becomes the bottleneck.
  3. The application server is scaled.
  4. Network bandwidth becomes the bottleneck.
  5. The network is improved.
  6. Lock contention becomes the bottleneck.

Optimization is not a one-time activity.

The constraint moves.

Organizational bottlenecks

Constraints are not always technical.

A software organization may be limited by:

  • slow code review
  • manual testing
  • security approval
  • unclear requirements
  • unavailable domain experts
  • fragmented ownership
  • long deployment windows
  • procurement delays
  • knowledge concentration

Buying faster servers does not solve a deployment-approval bottleneck.

Systems thinking examines the complete value-delivery flow.


Part X: Local Optimization and Global Harm

A local optimization improves one part of a system.

A global optimization improves the behavior of the entire system.

These are not always the same.

Example: aggressive caching

A team may add extensive caching to reduce database reads.

Locally, the service becomes faster.

Globally, the system may gain:

  • stale data
  • invalidation complexity
  • larger memory usage
  • unpredictable consistency
  • cache stampedes
  • harder debugging
  • hidden database overload after cache failure

The optimization may still be worthwhile, but it must be evaluated systemically.

Example: batching

Batching improves throughput by processing many items together.

However, it may also:

  • increase latency for individual items
  • increase memory usage
  • make failure recovery more complex
  • produce bursty downstream load
  • delay visibility
  • complicate cancellation

Example: asynchronous processing

Moving work to a background queue may improve request latency.

It can also introduce:

  • eventual consistency
  • duplicate processing
  • delayed failure
  • queue growth
  • retry complexity
  • observability challenges
  • ordering issues

The right question is not whether asynchronous processing is good.

The question is whether its total system effects match the requirements.


Part XI: Leverage Points

A leverage point is a place where a relatively small intervention produces a large change in system behavior.

Not all changes have equal power.

Low-leverage interventions

Examples include:

  • adjusting a small constant
  • adding more logging
  • increasing a timeout
  • adding more servers
  • optimizing an unimportant function

These may help, but they often treat symptoms.

Higher-leverage interventions

Examples include:

  • changing retry behavior
  • introducing backpressure
  • reducing coupling
  • changing ownership boundaries
  • improving information flow
  • redefining success metrics
  • removing unnecessary work
  • changing an API contract
  • simplifying deployment
  • making failure visible earlier

Example: information flow

Suppose developers do not know how their code behaves in production.

Adding a dashboard that shows:

  • error rate
  • latency
  • resource usage
  • deployment markers
  • customer impact

may influence hundreds of future decisions.

The dashboard changes information flow.

That may be more powerful than one isolated performance fix.

Example: changing incentives

Suppose teams are rewarded only for feature delivery.

Reliability work is neglected.

Changing performance evaluation to include:

  • incident reduction
  • maintainability
  • operational quality
  • customer outcomes

may alter the entire development system.

This is a deeper leverage point than adding another process rule.


Part XII: Time Horizons and Long-Term Dynamics

Software decisions create effects across different time horizons.

Immediate effects

  • feature is delivered
  • latency decreases
  • build passes
  • deployment succeeds

Medium-term effects

  • maintenance burden changes
  • debugging becomes easier or harder
  • dependencies require upgrades
  • team ownership shifts
  • customer behavior adapts

Long-term effects

  • architecture becomes rigid
  • knowledge is lost
  • technical debt accumulates
  • security assumptions become outdated
  • platforms are replaced
  • organizational structure changes

A decision that appears optimal today may create serious long-term cost.

Reversibility

One useful systems-thinking question is:

How difficult will this decision be to reverse?

Examples of relatively reversible decisions:

  • changing an internal helper function
  • adjusting a private algorithm
  • modifying a local build option

Examples of expensive decisions:

  • publishing a public API
  • choosing a persistent data format
  • adopting a cloud-specific architecture
  • splitting a monolith into services
  • selecting an identity model
  • defining a network protocol

The less reversible a decision is, the more systemic analysis it deserves.


Part XIII: Architecture Through a Systems Lens

Software architecture is not merely the arrangement of components.

It is the structure of important decisions, constraints, responsibilities, and interactions.

A systems-oriented architectural analysis considers:

  • functionality
  • performance
  • scalability
  • reliability
  • security
  • maintainability
  • deployability
  • observability
  • testability
  • organizational ownership
  • cost
  • compliance
  • evolution

Monoliths

A monolithic application packages much of the system into one deployable unit.

Potential advantages include:

  • simpler deployment
  • easier local development
  • straightforward transactions
  • lower network overhead
  • simpler debugging
  • fewer operational components

Potential disadvantages include:

  • larger deployment scope
  • weaker ownership boundaries
  • slower independent scaling
  • stronger coupling if poorly designed
  • risk of becoming difficult to understand

A monolith is not inherently bad.

A badly structured monolith is bad.

Microservices

Microservices divide the system into independently deployable services.

Potential advantages include:

  • independent deployment
  • independent scaling
  • clearer ownership
  • technological flexibility
  • fault isolation

Potential disadvantages include:

  • network latency
  • distributed failure
  • operational complexity
  • data consistency challenges
  • service discovery
  • tracing difficulty
  • versioning
  • higher infrastructure cost
  • more complex testing

Microservices move complexity rather than eliminating it.

They often move complexity from code structure into operations, networking, deployment, and coordination.

Modular monoliths

A modular monolith attempts to preserve strong internal boundaries while keeping one deployment unit.

This can provide:

  • simpler operations
  • explicit module ownership
  • reduced network complexity
  • easier later extraction

Systems thinking avoids choosing an architecture based on fashion. It evaluates the entire environment.


Part XIV: Distributed Systems

Distributed systems are systems in which components communicate over a network and may fail independently.

They are a major area where systems thinking is essential.

Partial failure

In a single process, failure is often obvious.

In a distributed system:

  • one service may fail
  • another may become slow
  • the network may drop packets
  • a request may succeed but the response may be lost
  • one region may be unavailable
  • clocks may disagree
  • some nodes may see stale data

The system can be partly alive and partly broken.

Time and uncertainty

A timeout does not prove that an operation failed.

It proves only that a response was not received before the deadline.

The operation may have:

  • failed
  • succeeded
  • still been running
  • succeeded but lost its response

This ambiguity is central to distributed systems.

Idempotency

An operation is idempotent when repeating it has the same intended effect as performing it once.

Idempotency is important when retries are possible.

For example, creating a payment without an idempotency key may charge the customer twice.

A systems thinker recognizes that retry policy, API design, storage, and business semantics must work together.

Consistency

Distributed systems often trade strong consistency for availability, latency, or scalability.

Possible models include:

  • linearizability
  • serializability
  • causal consistency
  • eventual consistency
  • read-your-writes consistency

The right model depends on the domain.

A social-media like counter may tolerate temporary inconsistency.

A bank-account transfer may not.


Part XV: Reliability and Resilience

Reliability is the ability of a system to perform its intended function over time.

Resilience is the ability to continue operating, recover, or degrade gracefully under failure.

Failure is normal

Large systems experience continuous failure.

Examples include:

  • machines reboot
  • disks fail
  • packets are lost
  • certificates expire
  • processes crash
  • deployments contain bugs
  • users submit unexpected input
  • dependencies become unavailable
  • networks partition
  • operators make mistakes

A resilient design assumes these events will happen.

Redundancy

Redundancy can improve availability.

Examples include:

  • replicated servers
  • replicated databases
  • multiple network paths
  • multiple availability zones
  • failover systems
  • backups

However, redundancy introduces complexity.

Replicas must be:

  • synchronized
  • monitored
  • tested
  • failed over
  • recovered

A backup that has never been restored is only an assumption.

Graceful degradation

A system should ideally preserve its most important functions during partial failure.

For an online store:

  • recommendations may fail
  • analytics may be delayed
  • promotional banners may disappear

but checkout should still work.

This requires prioritization.

Not every subsystem deserves equal protection.

Error budgets

An error budget translates reliability targets into an allowable amount of failure.

For example, a service-level objective may require 99.9 percent availability.

The remaining 0.1 percent becomes the error budget.

This creates a balancing mechanism between reliability and delivery speed.


Part XVI: Observability

Observability is the ability to infer the internal state of a system from its outputs.

A system may be running but still be difficult to understand.

Logging

Logs record events.

Useful logs may include:

  • timestamps
  • request identifiers
  • error details
  • state transitions
  • dependency calls
  • security-relevant events

Poor logging can create:

  • excessive noise
  • high storage cost
  • missing context
  • privacy exposure
  • performance overhead

Metrics

Metrics summarize behavior numerically.

Examples include:

  • request rate
  • error rate
  • latency
  • CPU usage
  • memory usage
  • queue depth
  • cache-hit rate
  • connection count

Averages alone are often misleading.

Latency distributions matter because a small fraction of very slow requests may severely affect users.

Tracing

Distributed tracing follows a request across services.

It helps answer:

  • where time was spent
  • which dependency failed
  • how many services participated
  • where retries occurred
  • where context was lost

Observability as system design

Observability should not be added only after failure.

It should influence architecture.

Questions include:

  • Can requests be correlated?
  • Can state transitions be reconstructed?
  • Can important business operations be traced?
  • Are failures distinguishable?
  • Can operators identify saturation before collapse?

A system that cannot explain itself is expensive to operate.


Part XVII: Security as a System Property

Security is not a feature attached to one module.

It is a system property.

A system may use strong encryption and still be insecure because of:

  • weak authorization
  • leaked secrets
  • insecure defaults
  • excessive privileges
  • vulnerable dependencies
  • misleading user interfaces
  • poor logging
  • unsafe recovery procedures
  • social engineering

Attack surfaces

Every interface creates an attack surface.

Examples include:

  • APIs
  • network ports
  • file formats
  • administrative consoles
  • update mechanisms
  • authentication flows
  • dependency supply chains
  • logging pipelines

Least privilege

Components should receive only the privileges they require.

This limits the effect of compromise.

However, least privilege requires:

  • clear ownership
  • permission modeling
  • secret management
  • auditing
  • maintenance

Security controls that are too difficult to use may be bypassed.

Again, human behavior is part of the system.

Defense in depth

No single control should be assumed perfect.

Layers may include:

  • input validation
  • authentication
  • authorization
  • isolation
  • encryption
  • monitoring
  • rate limiting
  • backups
  • incident response

The goal is not to guarantee that no control fails.

The goal is to prevent one failure from becoming catastrophic.


Part XVIII: Debugging with Systems Thinking

Traditional debugging often asks:

Which line of code is wrong?

Systems debugging asks:

What sequence of interactions produced this behavior?

Symptom versus cause

A database timeout may be caused by:

  • missing indexes
  • lock contention
  • exhausted connections
  • network latency
  • retry overload
  • memory pressure
  • slow storage
  • excessive logging
  • a deployment change
  • malformed queries
  • increased user traffic

The observed failure is not necessarily the root cause.

Root-cause analysis

Root-cause analysis attempts to understand deeper causes.

Useful questions include:

  • What changed?
  • When did the behavior begin?
  • Which conditions were necessary?
  • Why did safeguards fail?
  • Why was the issue not detected earlier?
  • Why did the system amplify the failure?
  • What would prevent recurrence?

A shallow analysis ends with:

The engineer introduced a bug.

A deeper analysis may discover:

  • tests did not cover the case
  • review lacked domain expertise
  • deployment provided no canary stage
  • monitoring did not detect the regression
  • rollback was slow
  • incentives rewarded delivery speed
  • ownership was unclear

The code defect may be only one cause among many.

Blameless analysis

Blameless incident review does not mean ignoring responsibility.

It means understanding that human actions occur within system conditions.

If one engineer can accidentally destroy production data with a single command, the system has a design problem.

The strongest corrective action is often not telling people to be more careful.

It is changing the system so the mistake is harder to make and easier to recover from.


Part XIX: Development Processes as Systems

Software development itself is a system.

Inputs include:

  • customer requests
  • bug reports
  • ideas
  • regulations
  • technical constraints

Processes include:

  • planning
  • design
  • implementation
  • review
  • testing
  • deployment
  • monitoring

Outputs include:

  • features
  • defects
  • documentation
  • operational load
  • customer value
  • technical debt

Feedback speed

Fast feedback helps systems adapt.

Examples include:

  • compiler diagnostics
  • unit tests
  • static analysis
  • code review
  • continuous integration
  • staging environments
  • canary deployment
  • production monitoring
  • customer feedback

The longer the feedback delay, the more expensive correction becomes.

A compiler error found in seconds is cheap.

A design flaw discovered after years of adoption may be extremely expensive.

Work in progress

Too much work in progress creates:

  • context switching
  • delayed delivery
  • merge conflicts
  • stale assumptions
  • coordination overhead
  • hidden queues

Limiting work in progress is a systems intervention.

It improves flow rather than merely making individuals work faster.

Agile and Lean

Agile and Lean practices are often described as project-management methods, but their deeper value lies in feedback and adaptation.

They emphasize:

  • small increments
  • rapid learning
  • customer feedback
  • reduced batch size
  • continuous improvement
  • visible work
  • limiting queues

When implemented mechanically, they can become bureaucracy.

The systems principle is more important than the ceremony.


Part XX: Metrics and Measurement

Metrics help engineers understand systems, but they can also distort them.

Goodhart's Law

A common principle states that when a measure becomes a target, it may cease to be a good measure.

Examples:

  • measuring lines of code encourages unnecessary code
  • measuring ticket count encourages splitting work artificially
  • measuring deployment frequency may encourage meaningless deployments
  • measuring uptime may encourage hiding incidents
  • measuring test count may encourage low-value tests

Leading and lagging indicators

Lagging indicators describe outcomes after they occur.

Examples:

  • outage duration
  • customer churn
  • revenue loss
  • defect count

Leading indicators may provide earlier warning.

Examples:

  • rising queue depth
  • increasing error rate
  • slower build time
  • growing dependency age
  • declining test reliability
  • increased on-call load

No single metric captures system health.

A useful measurement system uses multiple perspectives.


Part XXI: Mental Models and Analytical Tools

Systems thinking uses several tools to make complex relationships visible.

Causal-loop diagrams

A causal-loop diagram shows variables and their reinforcing or balancing relationships.

Example:

Service latency
    +
Retry volume
    +
Service load
    +
Service latency

The plus signs indicate that increases tend to cause increases.

Stock-and-flow diagrams

These show accumulation and movement.

For example:

Incoming requests ---> Request queue ---> Completed requests

The queue is a stock.

The arrival and completion rates are flows.

Dependency maps

Dependency maps show how services, libraries, databases, infrastructure, and teams depend on one another.

They help identify:

  • critical paths
  • single points of failure
  • ownership gaps
  • hidden coupling

Sequence diagrams

Sequence diagrams show interactions over time.

They are useful for:

  • distributed transactions
  • authentication flows
  • retries
  • timeouts
  • protocol behavior
  • concurrent operations

Fault trees

Fault-tree analysis begins with a failure and identifies combinations of causes that can produce it.

Failure mode and effects analysis

Failure mode and effects analysis examines:

  • how components can fail
  • what effects those failures produce
  • how failures are detected
  • how severe they are
  • how they can be mitigated

Wardley Mapping

Wardley Mapping is used to reason about value chains, user needs, and the evolution of components from novel to standardized.

It helps connect architecture with strategy.

Cynefin

The Cynefin framework distinguishes contexts such as:

  • clear
  • complicated
  • complex
  • chaotic

Different contexts require different approaches.

A familiar deterministic problem may benefit from analysis and expertise.

A complex adaptive problem may require experimentation and feedback.


Part XXII: What Is Systems Programming?

Systems programming is the engineering of software that provides foundational services, manages resources, or interacts closely with hardware and operating-system facilities.

Systems software often sits below application software.

Examples include:

  • operating systems
  • kernels
  • device drivers
  • compilers
  • linkers
  • loaders
  • language runtimes
  • network stacks
  • filesystems
  • database engines
  • hypervisors
  • container runtimes
  • embedded firmware
  • standard libraries
  • graphics engines
  • storage systems
  • command-line tools
  • performance-critical libraries

Systems programming is not defined only by language.

C and C++ are widely used, but systems programming can also be done in:

  • Rust
  • assembly
  • Zig
  • Ada
  • Go
  • modern C#
  • Java in some runtime and infrastructure contexts

The defining factor is the level of control and responsibility.


Part XXIII: Characteristics of Systems Software

Systems software often has demanding requirements.

Resource control

Systems programs frequently manage:

  • memory
  • file descriptors
  • sockets
  • threads
  • processes
  • CPU time
  • storage
  • devices
  • kernel objects
  • synchronization primitives

Resources are finite.

Failure to release them may degrade or crash the entire system.

Performance

Performance may matter in terms of:

  • throughput
  • average latency
  • tail latency
  • startup time
  • memory use
  • CPU use
  • cache efficiency
  • I/O efficiency
  • power consumption

Reliability

Systems software often supports many other programs.

A defect in a text editor may affect one document.

A defect in a kernel, runtime, database, or networking library may affect entire applications or machines.

Portability

Systems software often needs to support:

  • multiple operating systems
  • processor architectures
  • compilers
  • endianness
  • ABI differences
  • platform-specific APIs

Concurrency

Systems software frequently performs multiple operations concurrently.

This introduces:

  • race conditions
  • deadlocks
  • livelocks
  • starvation
  • memory-ordering issues
  • synchronization overhead

Security

Systems components often operate with elevated privilege or process sensitive data.

Memory errors, integer overflows, unsafe parsing, or incorrect permission checks can become serious vulnerabilities.


Part XXIV: Operating-System Foundations

Systems programmers must understand operating-system concepts.

Processes

A process is an executing program with:

  • virtual memory
  • resources
  • security context
  • threads
  • file descriptors
  • environment
  • execution state

Processes provide isolation.

A failure in one process is less likely to corrupt another.

Threads

Threads share process memory.

They allow concurrent execution but create synchronization challenges.

Shared mutable state must be coordinated.

Virtual memory

Virtual memory gives each process an address space.

It supports:

  • isolation
  • memory mapping
  • paging
  • copy-on-write
  • shared memory
  • protection

An address used by a program is typically a virtual address, not a direct physical-memory address.

System calls

A system call requests a service from the kernel.

Examples include:

  • opening files
  • creating sockets
  • mapping memory
  • starting processes
  • waiting for events
  • changing permissions

System calls cross the user-kernel boundary and may be more expensive than ordinary function calls.

Scheduling

The operating system decides which threads run and when.

Scheduling affects:

  • latency
  • throughput
  • fairness
  • responsiveness
  • real-time behavior

A program does not control the processor continuously unless it operates in a specialized environment.


Part XXV: Memory Management

Memory management is central to systems programming.

Stack and heap

The stack commonly stores:

  • local variables
  • return addresses
  • function-call state

The heap supports dynamic allocation.

Heap allocation provides flexibility but introduces:

  • allocation overhead
  • fragmentation
  • ownership complexity
  • possible leaks
  • unpredictable latency

Ownership

Ownership determines which part of a program is responsible for releasing a resource.

In modern C++, ownership is commonly represented through:

  • automatic storage
  • RAII
  • std::unique_ptr
  • std::shared_ptr
  • move semantics
  • containers
  • views such as std::span

Clear ownership is a system-design property, not merely a syntax choice.

Memory locality

Modern processors are much faster than main memory.

Performance depends heavily on locality.

Cache-friendly data structures may outperform theoretically elegant structures because they reduce memory latency.

Examples include:

  • contiguous arrays
  • structure-of-arrays layouts
  • compact object representations
  • reduced pointer chasing
  • cache-aware batching

NUMA

In non-uniform memory access systems, memory-access cost depends on which processor accesses which memory.

Systems programmers working on high-performance servers may need to consider:

  • thread affinity
  • memory placement
  • cross-socket traffic
  • allocator behavior

Part XXVI: Concurrency and Synchronization

Concurrency allows multiple tasks to make progress within overlapping time periods.

Parallelism means tasks actually execute simultaneously.

Race conditions

A race condition occurs when program behavior depends incorrectly on the timing of concurrent operations.

Example:

Thread A reads counter = 5
Thread B reads counter = 5
Thread A writes 6
Thread B writes 6

Two increments produce one result.

Mutexes

A mutex protects shared state by allowing one thread to enter a critical section at a time.

Mutexes simplify correctness but may cause:

  • contention
  • priority inversion
  • deadlocks
  • reduced parallelism

Deadlocks

A deadlock can occur when threads wait indefinitely for resources held by one another.

A common pattern is:

Thread A holds Lock 1 and waits for Lock 2
Thread B holds Lock 2 and waits for Lock 1

Prevention techniques include:

  • global lock ordering
  • lock hierarchies
  • scoped locking
  • minimizing lock scope
  • avoiding nested locking
  • message passing

Atomics

Atomic operations support synchronization without ordinary locks.

However, lock-free code is difficult.

Correctness may depend on:

  • memory ordering
  • visibility
  • ABA problems
  • reclamation
  • architecture-specific behavior

Lock-free does not automatically mean faster.

Thread pools

Creating one thread per task may be expensive.

Thread pools amortize creation cost and bound concurrency.

However, an unbounded task queue can still overload the system.

A systems-oriented thread pool requires:

  • queue limits
  • rejection policy
  • prioritization
  • shutdown semantics
  • observability
  • backpressure

Part XXVII: Networking and Systems Programming

Networking is one of the clearest intersections of systems thinking and systems programming.

Sockets

A socket is an operating-system abstraction for communication.

Common socket types include:

  • TCP sockets
  • UDP sockets
  • UNIX-domain sockets
  • raw sockets

A socket library must manage:

  • creation
  • binding
  • connection
  • sending
  • receiving
  • shutdown
  • closure
  • errors
  • timeouts
  • blocking mode
  • address conversion

TCP

TCP provides a reliable ordered byte stream.

It does not preserve application message boundaries.

A call to send() does not necessarily correspond to one call to recv().

Applications must define framing.

TCP also includes:

  • retransmission
  • congestion control
  • flow control
  • sequence numbers
  • acknowledgments

UDP

UDP provides datagrams without reliable delivery, ordering, or duplicate suppression.

It is useful when:

  • low latency matters
  • the application handles loss
  • message boundaries are useful
  • multicast or broadcast is needed
  • connection setup is undesirable

Blocking and non-blocking I/O

Blocking I/O waits until an operation can make progress.

Non-blocking I/O returns immediately when progress is unavailable.

Non-blocking designs often use:

  • select
  • poll
  • epoll
  • kqueue
  • I/O completion ports
  • asynchronous runtimes

Backpressure

Backpressure occurs when downstream capacity limits upstream production.

Without backpressure:

  • buffers grow
  • memory usage rises
  • latency increases
  • failures cascade

Network systems need explicit strategies for overload.


Part XXVIII: A C++ Socket Library as a Systems Case Study

Consider the design of a modern C++ socket library.

A narrow implementation view focuses on methods such as:

  • connect()
  • bind()
  • listen()
  • accept()
  • send()
  • receive()
  • close()

A systems-oriented view asks much more.

Resource ownership

Who owns the native socket handle?

Can it be copied?

Can it be moved?

What happens after a failed move?

Is closure guaranteed during exceptions?

RAII provides a strong answer by binding resource lifetime to object lifetime.

Error semantics

Operating systems report errors differently.

A portable library must decide:

  • how errors are represented
  • whether error codes are preserved
  • whether exceptions are used
  • how platform-specific messages are translated
  • which conditions represent timeouts
  • which operations may be retried

Timeout semantics

A timeout is not just a duration.

Questions include:

  • Does it apply to the whole operation or each internal attempt?
  • Is it implemented through socket options or non-blocking polling?
  • Does DNS resolution count?
  • What happens after timeout?
  • Is the socket still usable?
  • Can the operation be cancelled?

Blocking mode

Temporary non-blocking operations may be required for connection timeouts.

The library must preserve the previous mode even when exceptions occur.

This is a resource-state problem.

A scoped guard is often appropriate.

Buffering

Internal buffering may improve convenience and performance, but it introduces:

  • memory use
  • partial-read semantics
  • message-framing issues
  • interaction with timeouts
  • state that must survive between calls

Cross-platform behavior

Windows and POSIX networking differ in:

  • initialization
  • error codes
  • handle types
  • close functions
  • event mechanisms
  • socket-option behavior

A portable API should hide accidental differences without hiding meaningful semantic differences.

API usability

A technically correct API may still be unsafe or confusing.

Questions include:

  • Are ownership rules obvious?
  • Are invalid states representable?
  • Are timeouts explicit?
  • Are buffers safely represented?
  • Are operations easy to compose?
  • Is behavior documented precisely?

The system includes the programmer using the library.


Part XXIX: Compilers and Toolchains

Compilers are classic systems software.

A compiler may contain:

  • lexical analysis
  • parsing
  • semantic analysis
  • intermediate representation
  • optimization
  • code generation
  • assembly
  • linking

Toolchain interactions

Compilation also depends on:

  • headers
  • modules
  • libraries
  • ABI rules
  • linker scripts
  • debug information
  • build systems
  • package managers
  • platform SDKs

A compile error may originate from interactions between these layers.

Optimization

Compiler optimization illustrates systems trade-offs.

An optimization may improve runtime performance while increasing:

  • compile time
  • binary size
  • memory use
  • debugging difficulty

The appropriate optimization depends on the full context.


Part XXX: Storage Systems and Databases

Database engines are systems software because they manage:

  • storage
  • memory
  • concurrency
  • transactions
  • recovery
  • indexing
  • replication
  • caching

Durability

Durability often depends on write-ahead logging.

A transaction is recorded in a log before modified pages are considered durable.

But durability also depends on:

  • operating-system buffering
  • filesystem behavior
  • device caches
  • flush semantics
  • hardware guarantees

A single high-level write() call does not necessarily mean data is physically durable.

Transactions

Transactions provide properties commonly summarized as ACID:

  • atomicity
  • consistency
  • isolation
  • durability

These properties are implemented through complex mechanisms such as:

  • locks
  • multiversion concurrency control
  • logs
  • checkpoints
  • recovery procedures

Indexes

Indexes improve read performance but cost:

  • storage
  • memory
  • write performance
  • maintenance
  • planning complexity

Every index is a trade-off.


Part XXXI: Embedded and Real-Time Systems

Embedded systems run inside devices.

Examples include:

  • vehicles
  • medical devices
  • industrial controllers
  • appliances
  • sensors
  • communication equipment

Constraints

Embedded systems may have strict limits on:

  • memory
  • power
  • CPU
  • storage
  • network access

Real-time behavior

A real-time system is defined not only by producing correct results but by producing them within required timing constraints.

Hard real-time systems cannot tolerate missed deadlines.

Soft real-time systems may tolerate occasional misses but suffer degraded quality.

Predictability may matter more than average speed.

Dynamic allocation, garbage collection, and unbounded queues may be unsuitable when latency must be tightly controlled.


Part XXXII: Systems Programming Languages

No language is universally best.

The appropriate language depends on the system.

C

C provides:

  • direct memory access
  • predictable data representation
  • minimal runtime
  • broad platform support

It also provides limited protection against:

  • memory corruption
  • use-after-free
  • buffer overflow
  • ownership mistakes

C++

C++ adds:

  • RAII
  • templates
  • generic programming
  • classes
  • stronger abstractions
  • zero-cost abstractions
  • standard containers
  • modern concurrency support

Its complexity requires discipline.

Good C++ systems code often emphasizes:

  • clear ownership
  • value semantics
  • move semantics
  • bounded lifetimes
  • strong invariants
  • exception safety
  • careful interfaces

Rust

Rust emphasizes memory safety and data-race prevention through its ownership and type systems.

It reduces many classes of error but introduces:

  • a difficult learning curve
  • ecosystem maturity differences
  • interoperability challenges
  • complexity around advanced ownership patterns

Go

Go provides:

  • simple syntax
  • garbage collection
  • lightweight concurrency
  • a strong standard library
  • fast development

It may be less suitable where:

  • deterministic latency is essential
  • memory layout requires precise control
  • runtime overhead must be minimized

Language choice is a systems decision, not an identity.


Part XXXIII: Abstraction and Zero-Cost Design

Systems programming does not mean avoiding abstraction.

It means understanding its cost and behavior.

A useful abstraction should:

  • simplify reasoning
  • preserve important control
  • prevent invalid use
  • minimize hidden overhead
  • communicate ownership
  • improve maintainability

Zero-cost abstractions

A zero-cost abstraction aims to provide high-level expressiveness without additional runtime cost compared with a manual implementation.

Examples in C++ may include:

  • iterators
  • templates
  • std::span
  • RAII wrappers
  • compile-time polymorphism

However, zero runtime cost does not mean zero cost overall.

An abstraction may increase:

  • compile time
  • binary size
  • conceptual complexity
  • debugging difficulty

Again, the full system matters.


Part XXXIV: Exception Safety and Failure Contracts

Systems components must define behavior under failure.

In C++, exception safety is often described through guarantees.

No-throw guarantee

The operation does not throw.

Strong guarantee

If the operation fails, the observable state remains unchanged.

Basic guarantee

If the operation fails, invariants remain valid, but state may change.

No guarantee

Failure may leave the object or system in an undefined or unusable state.

RAII is essential because it ensures resources are released during stack unwinding.

However, exception safety also applies to:

  • transaction state
  • sockets
  • files
  • locks
  • partial writes
  • configuration updates
  • distributed operations

A local strong guarantee may not create a distributed strong guarantee.


Part XXXV: Testing Systems Software

Systems software requires multiple layers of testing.

Unit tests

Unit tests verify local logic.

They are useful but insufficient.

Integration tests

Integration tests verify interaction with:

  • operating systems
  • filesystems
  • networks
  • databases
  • external libraries

Stress tests

Stress tests examine behavior under heavy load.

They may reveal:

  • contention
  • memory growth
  • race conditions
  • queue instability
  • performance cliffs

Fault-injection tests

Fault injection deliberately introduces:

  • timeouts
  • dropped packets
  • disk errors
  • partial writes
  • process crashes
  • dependency failures

Fuzz testing

Fuzz testing supplies unexpected input to parsers and APIs.

It is especially valuable for:

  • network protocols
  • file formats
  • compilers
  • serialization
  • security-sensitive code

Property-based testing

Property-based testing verifies general properties across many generated inputs.

Examples:

  • encode then decode returns the original value
  • moving an object preserves ownership invariants
  • repeated idempotent operations do not change the result
  • sorting output is ordered and preserves elements

Part XXXVI: Performance Engineering as Systems Work

Performance engineering is not simply writing fast code.

It is the measurement and improvement of system behavior.

Measure first

Useful tools include:

  • profilers
  • flame graphs
  • hardware counters
  • tracing
  • load generators
  • heap analyzers
  • network captures
  • database query plans

Throughput and latency

Throughput measures work per unit time.

Latency measures time per operation.

Improving one can harm the other.

Batching may improve throughput while increasing latency.

Tail latency

Users may care more about slow requests than average requests.

The 99th percentile may reveal problems hidden by the mean.

Tail latency can be caused by:

  • queueing
  • garbage collection
  • page faults
  • lock contention
  • network retransmission
  • background work
  • noisy neighbors

Coordinated omission

Load tests can accidentally hide latency when the generator stops sending requests while the system is slow.

A realistic test must model arrival independently from completion.

This is another example of how measurement itself is part of the system.


Part XXXVII: Capacity Planning

Capacity planning asks whether a system can handle expected demand.

It considers:

  • request rate
  • data growth
  • concurrency
  • CPU
  • memory
  • storage
  • network
  • dependency limits
  • failure scenarios

Headroom

Operating permanently near maximum capacity is dangerous.

Variability requires headroom.

Load may increase because of:

  • traffic spikes
  • retries
  • failover
  • background jobs
  • deployments
  • attacks
  • regional shifts

Queuing behavior

As utilization approaches full capacity, waiting time can rise sharply.

A service at 50 percent utilization may respond quickly.

The same service at 95 percent utilization may become unstable even though nominal capacity has not been exceeded.


Part XXXVIII: Evolution and Maintainability

Software is maintained far longer than it is initially written.

Systems thinking therefore values evolution.

Change amplification

A change has high amplification when a small requirement requires edits across many components.

This often indicates coupling.

Compatibility

Compatibility includes:

  • source compatibility
  • binary compatibility
  • protocol compatibility
  • data compatibility
  • behavioral compatibility

Public interfaces are expensive because users may depend on undocumented behavior.

Migration design

Large changes often require staged migration.

A safe migration may involve:

  1. adding support for both formats
  2. deploying readers
  3. deploying writers
  4. migrating data
  5. monitoring
  6. removing old support

The system must remain valid during transition, not just before and after.


Part XXXIX: Common Failures of Systems Thinking

Systems thinking can also be misused.

Analysis paralysis

An engineer may attempt to understand every possible interaction before acting.

Complex systems can never be modeled completely.

The goal is not perfect prediction.

The goal is better decisions, safer experiments, and faster learning.

Excessive abstraction

Trying to design for every future scenario may create unusable abstractions.

Systems thinking should support adaptability, not speculative complexity.

Boundary inflation

If everything is treated as part of the system, analysis becomes impossible.

Useful systems thinking selects an appropriate boundary.

Ignoring implementation detail

A broad architectural view can become vague.

Low-level facts still matter.

A single incorrect memory-ordering assumption can invalidate an elegant architecture.

Systems thinking must coexist with technical precision.


Part XL: Practical Questions for Engineers

Before making a change, ask:

System boundary

  • What system am I analyzing?
  • What is outside the boundary?
  • Should the boundary be expanded?

Stakeholders

  • Who uses the system?
  • Who operates it?
  • Who maintains it?
  • Who pays for it?
  • Who is harmed if it fails?

Dependencies

  • What does this component depend on?
  • What depends on it?
  • Are dependencies synchronous or asynchronous?
  • Are there hidden shared resources?

State

  • Where is state stored?
  • Who owns it?
  • How is it synchronized?
  • How is it recovered?

Failure

  • What can fail?
  • How is failure detected?
  • Can the system recover?
  • Can failure cascade?
  • Is the operation safe to retry?

Performance

  • What is the bottleneck?
  • What is the expected load?
  • What happens at saturation?
  • Is backpressure present?
  • What do tail latencies look like?

Security

  • What are the trust boundaries?
  • What privileges are required?
  • What input is untrusted?
  • What happens after compromise?

Operations

  • How is the system deployed?
  • How is it monitored?
  • How is it rolled back?
  • Who responds to incidents?

Evolution

  • How reversible is the decision?
  • How will the system migrate?
  • What compatibility must be preserved?
  • What maintenance burden is introduced?

Part XLI: How to Develop Systems Thinking

Systems thinking is a skill that improves through practice.

Study incidents

Read incident reports.

Ask:

  • what triggered the event
  • what amplified it
  • why detection was delayed
  • why recovery was difficult
  • which safeguards failed
  • which organizational conditions mattered

Trace complete requests

Follow a request from:

  • user action
  • frontend
  • network
  • API
  • authentication
  • database
  • queue
  • external dependency
  • response

Measure time and failure at each stage.

Draw diagrams

Useful diagrams include:

  • dependency graphs
  • sequence diagrams
  • state machines
  • causal loops
  • data-flow diagrams
  • deployment diagrams

The act of drawing often reveals missing assumptions.

Study operating systems

Operating-system knowledge makes hidden behavior visible.

Learn about:

  • processes
  • threads
  • virtual memory
  • scheduling
  • filesystems
  • sockets
  • system calls
  • synchronization
  • I/O

Study distributed systems

Learn about:

  • partial failure
  • consensus
  • replication
  • consistency
  • idempotency
  • partitioning
  • timeouts
  • retries
  • message delivery

Measure production behavior

Use:

  • metrics
  • traces
  • profiling
  • logs
  • experiments
  • load testing

Intuition is useful, but measurement must constrain it.

Think in time

Ask how behavior changes:

  • after one minute
  • after one day
  • after one year
  • under growth
  • under failure
  • during migration

Part XLII: The Relationship Between Systems Thinking and Systems Programming

Systems thinking and systems programming reinforce one another.

Systems programming teaches how the machine and operating system actually behave.

Systems thinking teaches how those behaviors interact with architecture, users, operations, organizations, and time.

Example: memory allocation

A systems programmer knows:

  • allocations may be expensive
  • fragmentation matters
  • ownership must be correct
  • caches affect performance

A systems thinker additionally asks:

  • how does allocation behavior change under load?
  • how does memory pressure affect other services?
  • how is memory usage observed?
  • what happens when the allocator fails?
  • how does the design affect API users?

Example: socket timeouts

A systems programmer knows how to configure non-blocking I/O and polling.

A systems thinker asks:

  • what deadline does the user operation require?
  • how do retries interact with the timeout?
  • what happens to partial state?
  • how does timeout behavior affect downstream load?
  • can operators distinguish network failure from service overload?

Example: locking

A systems programmer knows how to protect shared data.

A systems thinker asks:

  • does the lock create a bottleneck?
  • can priority inversion occur?
  • can one tenant block others?
  • how is contention measured?
  • should ownership boundaries be redesigned?

Part XLIII: Why These Skills Matter for Senior Engineers

Junior engineers are often evaluated mainly on implementation.

Senior engineers are increasingly responsible for system consequences.

They must reason about:

  • architecture
  • operations
  • reliability
  • organizational coordination
  • long-term cost
  • migration
  • risk
  • trade-offs
  • failure containment
  • user impact

A senior engineer does not merely solve the visible ticket.

They ask whether the ticket describes the real problem.

They consider whether the proposed solution creates new problems.

They identify which part of the system has the greatest leverage.

They communicate across technical and non-technical boundaries.

They understand both mechanism and consequence.


Part XLIV: Final Perspective

Systems thinking and systems programming address two essential dimensions of software engineering.

Systems thinking teaches us to see software as an interconnected, evolving socio-technical system. It emphasizes relationships, feedback loops, constraints, delays, accumulation, emergence, trade-offs, and long-term behavior.

Systems programming teaches us to build and understand the foundational machinery of computing. It deals with memory, processes, threads, operating systems, networks, storage, compilers, hardware, concurrency, performance, and resource ownership.

Together, they produce a powerful engineering perspective.

The systems programmer asks:

  • What does the machine do?
  • What does the operating system guarantee?
  • Who owns this resource?
  • What happens under concurrency?
  • What is the cost of this abstraction?
  • How does failure propagate through the implementation?

The systems thinker asks:

  • How does this component affect the whole?
  • What feedback loops exist?
  • Which constraints determine behavior?
  • What unintended consequences may appear?
  • How will this system evolve?
  • How do people, processes, and incentives shape the technology?

The most capable engineers learn to ask both sets of questions.

They can inspect the smallest implementation detail without losing sight of the complete system.

They can discuss business objectives without becoming detached from technical reality.

They can optimize local code while understanding global performance.

They can design abstractions while respecting hardware.

They can plan architecture while accounting for operations, users, security, maintenance, and organizational structure.

Ultimately, software quality does not emerge merely from correct functions.

It emerges from the interaction of correct code, sound architecture, reliable infrastructure, clear ownership, effective feedback, disciplined operations, realistic incentives, and continuous learning.

That is the central lesson of systems thinking.

And building the low-level foundations that make those systems possible is the work of systems programming.

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