<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://dmtopolog.com/feed.xml" rel="self" type="application/atom+xml" /><link href="http://dmtopolog.com/" rel="alternate" type="text/html" /><updated>2025-10-24T09:18:59+00:00</updated><id>http://dmtopolog.com/feed.xml</id><title type="html">topolog’s tech blog</title><subtitle>Things your mom didn&apos;t tell you about iOS</subtitle><author><name>topolog</name></author><entry><title type="html">Complexity part 7. Organisation.</title><link href="http://dmtopolog.com/complexity-7-organisation" rel="alternate" type="text/html" title="Complexity part 7. Organisation." /><published>2025-05-26T00:00:00+00:00</published><updated>2025-05-26T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-7-organisation</id><content type="html" xml:base="http://dmtopolog.com/complexity-7-organisation"><![CDATA[<p>Misalignment and miscommunication are major, often underestimated sources of complexity in software projects. While previous chapters explored complexity within the codebase and within ourselves, this one shifts focus to the social layer: how teams, stakeholders, and individuals interact — or fail to — and how that influences the systems we build.</p>

<h2 id="misalignment-when-we-dont-share-the-same-mental-model">Misalignment: When We Don’t Share the Same Mental Model</h2>

<p>Misalignment happens when people involved in a project hold different understandings of what problem they’re solving, what success looks like, or how to get there. It’s one of the most subtle yet pervasive sources of accidental complexity — and it often goes unnoticed until the cost shows up in code.</p>

<p>What starts as a simple feature request can become an entangled mess when stakeholders, designers, and developers all interpret the task differently. Without a shared mental model, we build systems that reflect our assumptions — not the actual problem.</p>

<h3 id="vision-misalignment">Vision Misalignment</h3>

<p>One of the most fundamental forms of misalignment is simply not agreeing on what we’re building or why. Different stakeholders — product managers, designers, developers, leadership — may have conflicting interpretations of the goals, user needs, or constraints.</p>

<ul>
  <li>Sometimes, there’s no clearly defined problem at all — just an assumption that something “should” be done.</li>
  <li>In other cases, long-term strategic goals clash with short-term business needs.</li>
  <li>“Business rules” might be defined in multiple places — and none of them match.</li>
  <li>Product decisions are driven by opinions or hunches rather than data, leading to design churn and unclear priorities.</li>
</ul>

<h3 id="misaligned-priorities">Misaligned Priorities</h3>

<p>Even when the vision is clear, different roles often optimize for different outcomes:</p>

<ul>
  <li>Developers want clean code and test coverage, assuming they’re building production-ready systems.</li>
  <li>Product managers focus on MVP speed and user validation — assuming the work is disposable.</li>
  <li>One team might be optimizing for performance, another for flexibility, a third for time-to-market.</li>
</ul>

<p>These differences aren’t inherently bad — they reflect healthy tension. But without explicit alignment, they lead to subtle mismatches in expectations, scope creep, and rushed compromises.</p>

<blockquote>
  <p>Example: A developer spends extra time implementing a flexible architecture for future extensibility. The PM, assuming this is a throwaway prototype, is frustrated by the delay — and the result pleases no one.</p>
</blockquote>

<h3 id="ambiguity-and-vague-requirements">Ambiguity and Vague Requirements</h3>

<p>A more granular form of misalignment appears in vague or ambiguous requirements. This often stems from incomplete transfer of context between those who define problems (PMs, designers) and those who implement solutions (developers).</p>

<ul>
  <li>Assumptions go unstated.</li>
  <li>Domain details are missing or under-explained.</li>
  <li>Critical constraints are implied, not documented.</li>
  <li>Non-functional requirements are forgotten (security, performance, accessibility)</li>
</ul>

<p>When this happens, developers are left to interpret the feature based on their own mental models. The result may function — but not match the intent.</p>

<blockquote>
  <p>Example: A ticket says “add a price filter.” The developer adds a min–max input field. Later it turns out the business needed predefined price brackets (e.g., “Under $100”), and the implementation doesn’t fit the UI or backend model.</p>
</blockquote>

<h3 id="organizational-silos">Organizational Silos</h3>

<p>Organizational silos are another form of misalignment — but instead of differing opinions, they stem from <strong>a lack of shared visibility</strong>. Different teams or roles work toward a common goal but do so in parallel, without enough coordination or understanding of each other’s work. Everyone might be technically aligned on <em>what</em> to build, yet disconnected on <em>how</em>, <em>why</em>, or <em>under which constraints</em>. The result is a fractured system that’s more complex than it needs to be — not because the problem is hard, but because the communication is sparse.</p>

<p>You’ll see this when frontend and backend teams design an API without agreeing on data structures. When product and engineering align on features but not on edge cases. When designers hand off polished mockups that assume capabilities the platform doesn’t support. Or when QA and devs test and release features on different timelines. Each of these scenarios leads to misfit parts that need glue code, translation layers, or workarounds — all of which increase complexity without adding business value.</p>

<p>Silos often emerge naturally as teams specialize or grow. But the core issue is always the same: <strong>lack of shared understanding in real time</strong>. When teams don’t talk early and often, they make assumptions. And when those assumptions diverge, complexity fills the gap — not with better solutions, but with fragile connectors that patch over the misalignment.</p>

<h3 id="consequences-of-misalignment">Consequences of Misalignment</h3>

<p>Misalignment doesn’t just lead to frustration or delays — it directly adds accidental complexity to the system. Here’s how:</p>
<ul>
  <li><strong>Redundant logic:</strong> Different teams or individuals implement the same rules in slightly different ways — leading to duplication, drift, and inconsistency across modules.</li>
  <li><strong>Glue code and workarounds:</strong> Misunderstandings between teams (e.g. between frontend and backend, or between services) often result in a tangle of adapters, transformers, and “glue” logic that doesn’t solve a business problem — it just reconciles mismatched assumptions.</li>
  <li><strong>Overgeneralized solutions:</strong> When trying to cover everyone’s interpretation of a problem, we often end up building something overly flexible — with extra flags, parameters, conditionals, or configuration options that few fully understand or use.</li>
  <li><strong>Rewrites and rework:</strong> Code built under the wrong assumptions often needs to be torn down and redone, introducing churn and technical debt that could’ve been avoided with clearer alignment from the start.</li>
  <li><strong>Hidden complexity:</strong> The reasons behind certain decisions become hard to trace, especially when they originate from conflicting or unclear expectations. Future developers are left guessing why something was built the way it was — and often afraid to touch it.</li>
</ul>

<blockquote>
  <p>❗ Misalignment doesn’t always show up as a visible bug. It shows up as extra logic, fragile workarounds, mismatched interfaces, and an uneasy feeling that “this is more complicated than it needs to be.”</p>
</blockquote>

<p>The bottom line: every misunderstanding upstream multiplies complexity downstream. The longer it goes unaddressed, the more code it takes to compensate for it.</p>

<h2 id="inconsistency-the-hidden-complexity-multiplier">Inconsistency: The Hidden Complexity Multiplier</h2>

<p>Inconsistency across a project or organization might seem like a minor issue — a formatting style here, a naming variation there — but over time, it becomes a silent multiplier of complexity. It introduces friction in understanding, navigating, and contributing to code, particularly in growing teams or large codebases. Even when the code is technically correct, inconsistency forces developers to re-learn patterns, second-guess intentions, or stop to decipher why something was done differently.</p>

<p>You’ll see it in mismatched naming conventions, structural patterns that vary between modules, or different implementations of the same logic across features. One part of the app uses async/await, another uses callbacks. Some modules use MVVM, others don’t follow any pattern. Even small differences — like how date formatting is handled or where configuration lives — can add up.</p>

<p>Why is inconsistency so expensive? Because humans rely heavily on <strong>pattern recognition</strong> to reduce cognitive load. When patterns are familiar and predictable, we move faster and with more confidence. But inconsistency breaks that flow and introduces extra cognitive effort.</p>

<p>Here’s how inconsistency increases complexity:</p>

<ul>
  <li><strong>Increased mental overhead</strong>: Developers must constantly switch mental models when moving between modules or teams that barely resemble one another.</li>
  <li><strong>More room for error</strong>: Inconsistent naming or structure makes it easier to misunderstand what a piece of code does — or miss important edge cases.</li>
  <li><strong>Harder navigation</strong>: Without shared structure or naming, finding where something lives in the codebase takes longer.</li>
  <li><strong>Redundant discussions and PR churn</strong>: Inconsistent style leads to debates in code reviews that could be solved by shared rules.</li>
  <li><strong>Slower onboarding</strong>: Newcomers have more to learn, more context to absorb, and more habits to unlearn when every part of the system looks and behaves differently.</li>
  <li><strong>Decision fatigue</strong>: Developers spend time making decisions about formatting, naming, or architecture that could be solved once through agreement or codestyle guides.</li>
</ul>

<p>Consistency is more than just aesthetic — it’s a design decision that makes software easier to understand, evolve, and scale. Establishing shared conventions, architectural patterns, naming rules, and tooling (like linters, templates, and CI pipelines) helps protect against unnecessary complexity. The goal isn’t rigid standardization, but clarity: when developers move across the codebase, they shouldn’t feel like they’ve entered a different world. Familiarity lowers mental load — and that’s one of the simplest ways to keep complexity in check.</p>

<h2 id="organizational-design-shapes-system-design">Organizational Design Shapes System Design</h2>

<p>One of the most profound — yet often overlooked — sources of complexity is how our teams are structured. As Conway’s Law famously states:</p>

<blockquote>
  <p>“Any organization that designs a system will produce a design whose structure is a copy of the organization’s communication structure.”</p>
</blockquote>

<p>In other words, your architecture mirrors your org chart. When teams are siloed, their systems become siloed. When teams struggle to collaborate, their code reflects that friction. And when teams are optimized for autonomy, their modules become independent — sometimes overly so.</p>

<p>This isn’t always bad. Conway’s Law isn’t a warning — it’s a description of reality. Aligning architecture with team boundaries can actually reduce coordination overhead. But the downside is clear: <strong>organizational complexity breeds system complexity</strong>. For example:</p>

<ul>
  <li>A feature that spans multiple teams may require coordination between frontend, backend, infrastructure, and design — each with their own roadmaps and priorities.</li>
  <li>To respect team boundaries, logic might be split across services or modules that aren’t naturally separate — introducing glue code, abstractions, and coordination mechanisms.</li>
  <li>Autonomy can lead to redundancy — different teams solving similar problems in different ways, with different tools or assumptions.</li>
</ul>

<p>What begins as a strategy to scale teams can lead to fragmented systems with duplicated logic, excessive generalization, and complex workflows that serve the organization’s shape more than the user’s needs.</p>

<p>Understanding this relationship helps us make better decisions. If we know the system will reflect the structure of our teams, we can shape those structures with intention. Cross-functional teams, clear shared ownership, and regular syncs between collaborators reduce the architectural scars of organizational misalignment.</p>

<p>And when reorganization isn’t feasible, <strong>awareness itself becomes a tool</strong>. If we know that our team boundaries introduce complexity, we can invest in simplifying the seams: shared contracts, well-defined APIs, good documentation, and thoughtful glue code. Because sometimes, the best way to fix the architecture… is to fix how we work together.</p>

<h2 id="the-alignmentchaos-spectrum">The Alignment–Chaos Spectrum</h2>

<p>When it comes to organizational and system design, we are always balancing between two forces: <strong>alignment</strong> and <strong>chaos</strong>.</p>

<ul>
  <li>Too little alignment, and we descend into chaos — duplicated solutions, incompatible modules, inconsistent APIs, and endless miscommunication.</li>
  <li>Too much alignment, and we fall into rigidity — stifling flexibility, creating one-size-fits-all solutions, and forcing awkward fits for problems that don’t belong in the same mold.</li>
</ul>

<p><strong>Neither extreme is healthy.</strong> Complexity grows both when teams are too independent and when they are too tightly bound.</p>

<p>In a startup, a bit of chaos is survivable — even necessary. Agility matters more than perfect structure. In a large corporation, alignment is critical to avoid duplicated effort and to maintain coherence across massive systems. But even there, too much standardization can become its own kind of complexity: developers spend more time conforming to rules than solving real problems.</p>

<p>Finding the right point on the alignment–chaos spectrum is not about enforcing rigid standards or letting everything evolve organically. It’s about <strong>choosing where consistency matters most</strong> — and where flexibility should be preserved.</p>

<p>Some helpful questions to guide these choices:</p>
<ul>
  <li>Which areas are critical for our product (UI, security, fast feature delivery, API design, rich documentation)?</li>
  <li>Where does inconsistency create real cognitive load for developers or users?</li>
  <li>Where does forced consistency create unnecessary complexity or cost?</li>
  <li>Which areas can tolerate divergence (internal module structure, small utility patterns)?</li>
</ul>

<p>Alignment should focus on <strong>principles and shared understanding</strong>, not on micromanaging every decision. For example:</p>
<ul>
  <li>Agreeing that all APIs should be versioned, even if different services use slightly different tooling.</li>
  <li>Agreeing that features should be modular and testable, without forcing the same exact folder structure everywhere.</li>
  <li>Agreen that all the features of the mobile app use common modules/libraries/approaches for networking, encryption, analytics, UI animation, and so on.</li>
</ul>

<p>When we treat alignment and autonomy not as absolutes but as a spectrum to be tuned based on context, we gain a powerful tool to manage complexity consciously — rather than reactively.</p>

<h3 id="the-challenge-of-scaling-alignment">The Challenge of Scaling Alignment</h3>

<p>When an organization grows, two natural forces come into play:</p>

<ul>
  <li><strong>Autonomy needs to increase</strong> to keep teams agile and motivated. Each team needs the space to make local decisions without endless cross-team negotiation.</li>
  <li><strong>Alignment becomes harder</strong> because there are more moving parts, more diverse needs, and longer communication paths.</li>
</ul>

<p>Finding the right balance between alignment and flexibility isn’t a one-time decision. As organizations scale, what once made sense at 5 people might break down at 50, and what worked at 50 might crumble at 500. New teams form, responsibilities shift, and technical debt compounds. Alignment must be treated as an evolving, living system — something we revisit regularly to keep complexity manageable without stifling autonomy.</p>

<h3 id="familiarity-is-not-simplicity">Familiarity Is Not Simplicity</h3>

<p>When you’ve been in the same organization or working on the same project for a long time, you naturally adapt to its quirks — even the overly complex ones. But just because something <em>feels</em> familiar doesn’t mean it’s <em>simple</em>. Familiarity masks complexity. You stop noticing the awkward workflows, overloaded abstractions, or brittle architecture — not because they’ve become better, but because you’ve built mental shortcuts around them.</p>

<p>That’s why it’s important to listen to newcomers. Fresh eyes often spot convoluted patterns or redundant processes that long-timers no longer question. If they point out areas of confusion or unnecessary indirection, take it seriously — it might be time to clean things up.</p>

<p>A related trap is unclear responsibility. When ownership boundaries aren’t well defined, work gets messy — not just in the code, but across the whole development process. And messy systems, organizational or technical, are always harder to navigate, reason about, and maintain.</p>

<h2 id="strategies-for-evolving-alignment">Strategies for Evolving Alignment</h2>

<ul>
  <li>
    <p><strong>Principle-Based Governance</strong><br />
Instead of hard rules for every decision, define a few strong principles that guide teams in making their own aligned choices (e.g., “Favor explicit over implicit dependencies” or “Prioritize local reasoning in APIs”).</p>
  </li>
  <li>
    <p><strong>Lightweight Alignment Forums</strong><br />
Create spaces (like architecture circles, guilds, or working groups) where interested developers discuss and update shared practices — without heavy-handed command-and-control.</p>
  </li>
  <li>
    <p><strong>Versioning and Layered Contracts</strong><br />
Allow systems to evolve by versioning APIs, internal modules and services, and shared libraries. Not everything has to align instantly — progressive adoption is key.</p>
  </li>
  <li>
    <p><strong>Clear Boundaries and Ownership</strong><br />
Explicitly define system ownership. When boundaries are clear, teams can innovate internally while maintaining stable interfaces externally.</p>
  </li>
  <li>
    <p><strong>Celebrating Simplicity</strong><br />
Recognize and reward teams that <em>remove</em> complexity, not just those who add impressive-looking abstractions. Make simplicity part of your technical culture.</p>
  </li>
</ul>

<p>Ultimately, alignment isn’t about creating the One True Way. It’s about minimizing the unnecessary complexity that grows when communication, expectations, and responsibilities drift apart — while leaving space for the necessary complexity that arises from solving real-world problems.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Misalignment, inconsistency, and siloed collaboration don’t just slow us down — they silently shape our systems, making them harder to understand, evolve, and maintain. Complexity doesn’t come only from within the code — it emerges from the spaces between people: from miscommunication, vague expectations, inconsistent decisions, and disconnected teams. These invisible dynamics leave very real traces in every layer of our systems.</p>

<p>If we want to reduce complexity, we can’t focus solely on technical design. We have to shape the environment in which software is designed. That means building strong feedback loops, aligning on principles over rigid processes, and fostering a culture of collaboration and mutual understanding. It means recognizing that organizational structure, communication practices, and cultural defaults all influence the clarity — or confusion — of what we build.</p>

<p>Every organization must find its own balance between alignment and chaos. Too little alignment, and inconsistency and redundancy take over. Too much, and flexibility disappears. But if we invest in clear boundaries, shared principles, and continuous conversation, alignment becomes not a constraint, but a force multiplier. The real challenge isn’t just writing better code — it’s creating the conditions where better code can be written.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="complexity" /><category term="platform-agnostic" /><category term="organisation" /><category term="no code" /><summary type="html"><![CDATA[Misalignment and miscommunication are major, often underestimated sources of complexity in software projects. While previous chapters explored complexity within the codebase and within ourselves, this one shifts focus to the social layer: how teams, stakeholders, and individuals interact — or fail to — and how that influences the systems we build.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-05-26-complexity-7-organisation/header.jpg" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-05-26-complexity-7-organisation/header.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 6. Human nature.</title><link href="http://dmtopolog.com/complexity-6-human-nature" rel="alternate" type="text/html" title="Complexity part 6. Human nature." /><published>2025-05-19T00:00:00+00:00</published><updated>2025-05-19T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-6-human-nature</id><content type="html" xml:base="http://dmtopolog.com/complexity-6-human-nature"><![CDATA[<p>When we talk about complexity in software, we often focus on technical causes — sprawling architectures, tight coupling, or poorly structured code. But underneath all of that lies a deeper source: human nature itself. The way we think, reason, decide, and collaborate directly shapes the systems we build. This article explores the human side of software complexity, not to eliminate it, but to understand it better and work with it more intentionally.</p>

<h2 id="cognitive-limits">Cognitive Limits</h2>

<p>The entire topic of complexity in software ultimately stems from a fundamental truth: our cognitive capacity is limited.</p>

<p>Our working memory can only hold a small number of items at once — a constraint famously known as the 7±2 rule. (<em>We’ve shared more on that in the <a href="/complexity-0-introduction">introduction to this topic</a></em>)</p>

<p>Because of this, we’re forced to organize code in ways that reduce the number of things we have to think about at the same time. That’s why we spend so much time discussing how to design our modules, structure abstractions, and align the level of detail with the context in which the code is used. (<em>We’ve explored these topics in earlier chapters: <a href="/complexity-1-decisions-in-code">part 1</a>, <a href="/complexity-2-logic-code-disctribution">part 2</a>, <a href="/complexity-3-problem-solution-mismatch">part 3</a>, <a href="/complexity-4-abstraction">part 4</a>, <a href="/complexity-5-interfaces">part 5</a>, check them out for a deeper dive.</em>)</p>

<p>But memory limitations are only one part of the story. There are several other traits of human cognition that shape how we write and experience code:</p>

<h3 id="difficulty-with-state-tracking">Difficulty with State Tracking</h3>

<p>Humans are notoriously bad at holding multiple possible states in mind — especially when those states are implicit. When reading code, we often have to mentally simulate different paths: “What happens if this flag is true?”, “Is this variable already set?”, “Which branch are we in right now?” As the number of possible states grows, our ability to reason about the system drops sharply. This is why deeply stateful systems, boolean flags, and side-effect-heavy logic can become so cognitively expensive — even when they’re technically correct.</p>

<h3 id="poor-handling-of-indirection">Poor Handling of Indirection</h3>

<p>Indirection is a powerful tool — we use it to make code modular, abstract, and reusable. But every level of indirection makes it harder to trace what’s actually happening. When a function calls a method on a dependency, which is a protocol-backed abstraction that calls another injected service, we’ve added multiple hops for the brain to follow. Protocols, callbacks, dependency injection, and decorators are all useful — but they come at a cost. Too much indirection breaks our intuitive grasp of control flow and turns even simple tasks into a scavenger hunt.</p>

<h3 id="context-switching-is-expensive">Context Switching Is Expensive</h3>

<p>Whenever we switch from one file, type, or layer to another, we need to load a new mental model. That mental juggling slows us down and increases the chance of mistakes. If your current task requires bouncing between the view, the model, two unrelated services, and a shared utility, the mental overhead becomes significant. This is why we care so much about <strong>cohesion</strong> and <strong>coupling</strong>. When related logic is kept close together, and unrelated logic is clearly separated, we can stay focused on a task without constantly reorienting ourselves.</p>

<h3 id="preference-for-linear-thinking">Preference for Linear Thinking</h3>

<p>We’re much better at understanding sequences than branches. Code that reads like a clear, step-by-step story is far easier to follow than logic that fans out into deeply nested conditionals or recursive calls. When possible, flat, linear flows reduce mental strain. This doesn’t mean avoiding branching entirely, but it does mean we should be mindful of how deep and how scattered that branching becomes — especially when the logic is spread across multiple files or functions.</p>

<h3 id="cognitive-fatigue">Cognitive Fatigue</h3>

<p>Even the clearest code becomes hard to work with when we’re tired, stressed, or under pressure — which is often the case in real development. What seems like a minor complexity in the morning can feel like a blocker at 6 PM after a day of meetings and bug triaging. This is why we should aim to write code that’s easy to understand not just in theory, but in practice — under less-than-ideal conditions. Clear, localized, well-named logic helps future you (or your teammates) work more effectively when mental energy is low.</p>

<hr />

<h2 id="cognitive-biases">Cognitive Biases</h2>

<p>All human beings are biased. We don’t perceive the world exactly as it is — we filter it through years of experience, instinct, and evolved mental shortcuts. These shortcuts, known as <strong>cognitive biases</strong>, help us make decisions quickly and efficiently, especially under uncertainty. They evolved to help our ancestors survive — to avoid danger, spot opportunities, and act fast when time mattered most.</p>

<p>In many real-world situations, these instincts are helpful. They allow us to judge quickly, act decisively, and reduce decision fatigue. But in software development — where the problems are abstract, the consequences delayed, and the systems complex — these same instincts can easily mislead us.</p>

<p><img src="/images-posts/2025-05-19-complexity-6-human-nature/cognitive-biases.png" alt="" /></p>

<p>Books like <em>Thinking, Fast and Slow</em> by Daniel Kahneman and <em>Predictably Irrational</em> by Dan Ariely explore the nature of these biases in depth, revealing just how irrational even our most “logical” thinking can be. Let’s look at a few common cognitive biases and how they influence the decisions we make in our work — often adding accidental complexity to our systems.</p>

<h3 id="complexity-bias">Complexity Bias</h3>

<p>We tend to assume that complex problems require complex solutions. Simple answers feel naive, incomplete, or unworthy of real-world use — even when they’re exactly what’s needed.</p>

<p>For example, we might skip using a basic dictionary or an enum to model a small state machine and instead introduce a full-blown state pattern with protocols, coordinators, and factories — not because the problem demands it, but because it <em>feels</em> like a better-engineered solution. The result? More indirection, more surface area, and more mental effort to maintain something that could have been straightforward.</p>

<h3 id="simplicity-bias">Simplicity Bias</h3>

<p>At the other extreme, we sometimes believe that <em>one</em> simple idea can solve all our problems. This is the “silver bullet” mindset — believing that a new framework, language, or architecture will fix everything.</p>

<p>We see this when teams latch onto new technologies and try to apply them everywhere. But reality rarely conforms to a single tool. For instance, switching to a reactive programming framework might simplify some flows but introduce unnecessary complexity in others. Overcommitting to a universal solution often leads to misfit abstractions and workarounds that break the simplicity we were after.</p>

<h3 id="foreseeing-the-future">Foreseeing the Future</h3>

<p>Our ability to imagine the future helped humans survive. But in software, our predictions are often wrong.</p>

<p>We design for imagined scale, performance, and reusability — and build systems that are far more complex than they need to be. We add abstraction layers, generalize interfaces, or design elaborate plugin architectures for a feature that might never grow. The future-proof solution feels responsible in the moment — but if that future never arrives, we’re left maintaining complexity that no longer serves a purpose.</p>

<p>The same happens with <strong>premature optimization</strong>. We spend days crafting modular architecture, separating every concern, or making our scroll views buttery smooth — in an MVP no one has used yet. We optimize for problems we <em>might</em> face, rather than focusing on what matters now.</p>

<h3 id="conformism-and-cargo-cults">Conformism and Cargo Cults</h3>

<p>Humans are social creatures. We learn by imitation — especially from those we perceive as successful. But copying without understanding is risky.</p>

<p>In software, this manifests as <strong>cargo culting</strong> — adopting tools or practices simply because others (often big companies) do. One famous example: Airbnb adopted React Native for their mobile app, later found it misaligned with their needs, and moved away from it. Yet many teams continue to use React Native <em>because</em> Airbnb once did, not considering whether it’s right for their own scale and context.</p>

<h3 id="anchoring-bias">Anchoring Bias</h3>

<p>Once we make an initial choice, we tend to stick with it — even when the context changes.</p>

<p>Let’s say a team has used UIKit for a decade. Even as SwiftUI matures and becomes a better fit for new features, they continue to build everything the old way — not because it’s better, but because it’s familiar. The initial decision becomes an unspoken prerequisite. We stop evaluating it critically and treat it as a fixed point in our reasoning.</p>

<h3 id="confirmation-bias">Confirmation Bias</h3>

<p>We naturally seek information that supports what we already believe and ignore what contradicts it.</p>

<p>If we think a particular architecture is superior, we’ll highlight examples where it worked and dismiss the ones where it didn’t. This bias slows down learning and locks us into suboptimal patterns. It makes us blind to flaws in our own code and resistant to alternative perspectives — both of which are essential to reducing complexity.</p>

<h3 id="sunk-cost-fallacy">Sunk Cost Fallacy</h3>

<p>This bias leads us to continue investing in something because we’ve already put time, effort, or resources into it — even when it no longer makes sense.</p>

<p>For example, a team might recognize that Core Data is no longer meeting their needs. But because they’ve already built everything around it, switching feels like admitting failure or “wasting” past work. So they keep investing in the old system, layering on patches and workarounds, increasing complexity with every iteration — instead of making a clean break.</p>

<h3 id="pattern-recognition-and-misapplication">Pattern Recognition (and Misapplication)</h3>

<p>Our brains are wired to spot patterns — it’s how we learn. But sometimes we see patterns where none exist, or we apply old solutions to new problems that only <em>look</em> similar.</p>

<p>For instance, seeing that two features share a similar flow, we abstract their logic into a shared module. But small differences begin to emerge — and soon the abstraction becomes a tangle of conditionals trying to accommodate both. What started as an effort to simplify ends up more complex than the originals.</p>

<p><img src="/images-posts/2025-05-19-complexity-6-human-nature/subjective-reality-original.jpg" alt="" /></p>

<p>Biases shape our subjective reality, influencing our decision-making and problem-solving processes. Ultimately, they can lead us to incorrect understanding of the situation, wrong conclusions resulting in solutions that fail to fully address the problem and instead introduce unnecessary complexity.</p>

<hr />

<h2 id="developers-specifics-when-complexity-becomes-a-temptation">Developers’ Specifics: When Complexity Becomes a Temptation</h2>

<p>Beyond the general cognitive biases we all share, software developers have their own unique tendencies — shaped by the nature of the work, the culture of the industry, and the psychology of technical problem-solving. These tendencies don’t just tolerate complexity — they sometimes <em>invite</em> it.</p>

<h3 id="simple-solutions-are-boring">Simple Solutions Are Boring</h3>

<p>Many developers, especially experienced ones, find little excitement in straightforward answers. A problem that can be solved in ten lines of code might not feel “interesting” enough. Instead, we look for elegant patterns, clever abstractions, or novel techniques — not because they’re needed, but because they’re <em>fun</em>. This often results in solutions that are harder to read, test, or maintain — even though they were more enjoyable to build.</p>

<h3 id="the-god-syndrome">The God Syndrome</h3>

<p>There’s a subtle, almost mythic appeal to mastering complexity. Building something deeply abstract or highly optimized — something only you or a few others truly understand — can feel like a power move. The more sophisticated our code becomes, the more control and intellectual dominance we might feel. But this illusion of mastery can be dangerous: systems built for intellectual satisfaction often become fragile, opaque, and intimidating for others.</p>

<h3 id="social-reinforcement-of-complexity">Social Reinforcement of Complexity</h3>

<p>Complexity is frequently rewarded in various professional domains from academia to industry, and software development is also among them. In code reviews, presentations, or open-source projects, intricate solutions are often met with admiration — even when a simpler alternative would be more maintainable. In tech culture, solving something in a clever way is often valued more than solving it in a <em>clear</em> way. This dynamic reinforces habits that prioritize impressiveness over simplicity.</p>

<h3 id="reinventing-the-wheel">Reinventing the Wheel</h3>

<p>For many developers (myself included), solving problems from scratch is one of the most enjoyable parts of the job. Even when well-tested libraries, frameworks, or even components from our own projects already exist to solve the problem, we sometimes choose to build a new solution. We justify it as being more “tailored to our needs,” but it often duplicates effort and introduces new maintenance burdens. Reinvention gives us a sense of ownership and technical fulfillment — but it can also add unnecessary complexity to systems that didn’t require it.</p>

<h3 id="cv-driven-development">CV-Driven Development</h3>

<p>Sometimes, complexity isn’t introduced for technical reasons at all — but for career visibility. Adopting trendy architectures, experimental tools, or heavyweight frameworks can be justified as “future-proofing” or “scaling,” when in reality they’re resume boosters. We rationalize that these technologies will “look good on GitHub” or during interviews — even if they overcomplicate the current product.</p>

<hr />

<h2 id="personal-traits-and-complexity">Personal Traits and Complexity</h2>

<p>Beyond the biases, there’s another important factor influencing the complexity of our solutions — our individual personalities.</p>

<p>We all bring a unique mix of temperament, preferences, strengths, and fears to our work. These personal traits shape how we interpret problems, what kind of solutions we gravitate toward, and how we approach uncertainty. The result? Even with the same technical skills and constraints, two developers may solve the same problem in entirely different ways — with wildly different complexity profiles.</p>

<p>Here are some of the personality-driven behaviors that subtly (or not-so-subtly) influence the decisions we make.</p>

<h3 id="risk-tolerance-and-change-aversion">Risk Tolerance and Change Aversion</h3>

<p>Some developers thrive on experimentation, eager to adopt new tools, rewrite old modules, or rethink established structures. Others prefer stability — valuing predictability, reliability, and minimizing disruption.</p>

<ul>
  <li>High risk-tolerance can lead to innovation and architectural progress — but also to instability and churn.</li>
  <li>Low risk-tolerance protects against breaking changes, but can entrench legacy systems and block necessary evolution.</li>
</ul>

<blockquote>
  <p>🧩 <em>Example: One developer pushes to migrate a legacy feature to Swift Concurrency. Another resists, citing edge cases and team ramp-up time. The resulting compromise is a hybrid system that’s more complex than either approach on its own.</em></p>
</blockquote>

<h3 id="perfectionism-vs-pragmatism">Perfectionism vs. Pragmatism</h3>

<p>Perfectionists strive for clean abstractions, polished code, and comprehensive solutions — sometimes to the point of over-engineering. Pragmatists, on the other hand, optimize for getting things done with minimal friction.</p>

<ul>
  <li>Perfectionism can lead to elegant but bloated designs.</li>
  <li>Pragmatism can lead to faster delivery but more technical debt — especially when context is ignored.</li>
</ul>

<blockquote>
  <p>🧩 <em>Example: A team creates a generic plugin-based form engine that can handle any field type. The product only needs two. The overhead ends up outweighing the benefit.</em></p>
</blockquote>

<h3 id="detail-oriented-vs-big-picture-thinkers">Detail-Oriented vs. Big Picture Thinkers</h3>

<p>Some developers obsess over implementation detail — performance, edge cases, data layout. Others focus on architecture, workflows, and long-term design.</p>

<ul>
  <li>Detail-oriented devs may introduce low-level complexity in the name of precision.</li>
  <li>Big-picture thinkers may miss technical pitfalls while chasing architectural ideals.</li>
</ul>

<blockquote>
  <p>🧩 <em>Example: One dev optimizes data caching with custom flags and serialization. Another finds it too hard to use and starts building a parallel system — doubling the complexity.</em></p>
</blockquote>

<h3 id="control-oriented-vs-delegators">Control-Oriented vs. Delegators</h3>

<p>Control-oriented developers prefer to own the entire stack, building their own solutions for predictability. Delegators trust libraries, services, and abstraction to do the heavy lifting.</p>

<ul>
  <li>Too much control leads to reinvention and high maintenance.</li>
  <li>Too much delegation can result in black-box dependencies and unexpected edge cases.</li>
</ul>

<blockquote>
  <p>🧩 <em>Example: A team builds a custom HTTP client for “full control.” Later, they struggle to add standard features like retries and authentication that a third-party library already offered out of the box.</em></p>
</blockquote>

<h3 id="confidence-and-experience-level">Confidence and Experience Level</h3>

<p>Confidence shapes how developers make trade-offs. Less experienced developers may over-rely on frameworks or patterns they’ve seen in tutorials. More experienced ones may overgeneralize from past projects — even when the context has changed.</p>

<blockquote>
  <p>🧩 <em>Example: A junior developer uses a <code class="language-plaintext highlighter-rouge">BaseViewModel</code> pattern because it’s common in examples — even though the app doesn’t need it. A senior developer insists on custom DI infrastructure because “that’s what worked in the past,” ignoring better options.</em></p>
</blockquote>

<h3 id="communication-style-and-assertiveness">Communication Style and Assertiveness</h3>

<p>In team environments, not all decisions are made by the most thoughtful person — sometimes they’re made by the most vocal one. Developers who are confident and articulate may steer teams toward or away from certain solutions. Those who are quieter may struggle to challenge decisions — even when they spot emerging complexity.</p>

<blockquote>
  <p>🧩 <em>Example: A persuasive developer pushes Clean Architecture for a simple app. The team complies, but ends up with fragmented modules and ceremony-heavy code that no one enjoys working with.</em></p>
</blockquote>

<hr />

<h2 id="navigating-complexity-with-self-awareness">Navigating Complexity with Self-Awareness</h2>

<p>At its core, software complexity isn’t just a technical challenge — it’s a human one. Our cognitive limits, personal traits, biases, and emotional instincts are deeply woven into the systems we build. We like to imagine ourselves as rational, logic-driven engineers, but the truth is more nuanced. Every decision — from architecture to variable naming — is filtered through the lens of how we think, feel, percieve the world and relate to others.</p>

<h3 id="acknowledging-human-nature">Acknowledging Human Nature</h3>

<p>The first step toward managing this complexity is simple: <strong>awareness</strong>. You can’t remove cognitive biases or overcome your working memory limits — but you can notice them. By naming our instincts, we give ourselves a moment to pause, reflect, and respond with intention instead of habit. Recognizing that our brains are optimized for survival, not systems design, helps us work with our limitations rather than against them.</p>

<h3 id="recognize-the-invisible-forces">Recognize the Invisible Forces</h3>

<p>Before reaching for a solution, step back. Ask yourself: <em>Why am I really making this choice?</em><br />
Is this abstraction for clarity — or for cleverness?<br />
Am I resisting a change because it’s risky — or because it’s genuinely unnecessary?<br />
Am I repeating a familiar pattern just because it’s comfortable?</p>

<p>And then look beyond yourself:<br />
Is the technical direction being shaped by thoughtful discussion — or by the loudest voice in the room?<br />
Are diverse perspectives being heard, especially from those who may be quieter or newer to the team?</p>

<h3 id="build-systems-that-guide-behavior">Build Systems That Guide Behavior</h3>

<p>Not all complexity comes from individual behavior. Sometimes it’s baked into the culture, the process, or the tooling. That’s why we need to <strong>intentionally shape our environment</strong> to support good decisions:</p>

<ul>
  <li>Favor conventions and templates that encourage clarity over cleverness.</li>
  <li>Keep interfaces small, names specific, and responsibilities narrow by default.</li>
  <li>Use boundaries — both architectural and social — to prevent complexity from leaking across the system.</li>
</ul>

<h3 id="create-safe-spaces-for-discussion-and-learning">Create Safe Spaces for Discussion and Learning</h3>

<p>Healthy teams surface biases early, not late. They ask hard questions in design reviews, reflect together after launches, and encourage curiosity over defensiveness. Pairing different personality types — perfectionists with pragmatists, cautious thinkers with bold innovators — often reveals blind spots and balances extremes.</p>

<p>And most importantly: foster a culture where challenging an idea doesn’t feel like challenging a person. That’s how we move from defending complexity to dismantling it.</p>

<h3 id="use-structured-decision-making-tools">Use Structured Decision-Making Tools</h3>

<p>Cognitive shortcuts often lead us to pick familiar tools or repeat past solutions without critical evaluation. Checklists, decision matrices, or even just a shared “consider before you abstract” checklist can help you avoid these defaults and consider meaningful alternatives.</p>

<h3 id="rely-on-data-not-just-intuition">Rely on Data, Not Just Intuition</h3>

<p>Opinions are loud, but data is quieter and more reliable. When deciding whether to refactor, optimize, or adopt a new tool, ground the discussion in evidence: performance metrics, usage data, real-world constraints. Knowing the difference between a real pattern and a coincidental one is a superpower — and statistics are the map.</p>

<h3 id="apply-occams-razor">Apply Occam’s Razor</h3>

<p>Finally, when two solutions solve the same problem equally well, <strong>choose the simpler one</strong>. The one with fewer assumptions, fewer moving parts, and fewer things that can go wrong. Simplicity isn’t just elegance — it’s strategic clarity.</p>

<hr />

<h2 id="final-thoughts">Final thoughts</h2>

<p>Software complexity often looks like a technical problem — but it’s deeply human. It reflects how we think, how we collaborate, and how we cope with uncertainty. Code is written for humans first, machines second — and our brains are the ultimate interpreter. The best software isn’t just technically sound; it’s built by people who understand not only the system, but themselves. With self-awareness, empathy, and clear intent, we can create systems that are not only powerful, but also maintainable, adaptable, and human-friendly.</p>

<p>Cognitive limits, biases, and personal tendencies all influence the way we design and evolve software. They shape how we frame problems, what solutions we reach for, and which trade-offs we accept. Left unchecked, they lead to solutions that don’t quite fit — adding complexity not because the problem demanded it, but because we misunderstood the problem. When we recognize these human influences, we gain the power to catch ourselves — and each other — in the act of unintentionally making things harder. That’s how we move from managing code to managing complexity.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="complexity" /><category term="platform-agnostic" /><category term="human-factor" /><category term="psychology" /><category term="no code" /><summary type="html"><![CDATA[When we talk about complexity in software, we often focus on technical causes — sprawling architectures, tight coupling, or poorly structured code. But underneath all of that lies a deeper source: human nature itself. The way we think, reason, decide, and collaborate directly shapes the systems we build. This article explores the human side of software complexity, not to eliminate it, but to understand it better and work with it more intentionally.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-05-19-complexity-6-human-nature/header.jpg" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-05-19-complexity-6-human-nature/header.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 5. Interfaces.</title><link href="http://dmtopolog.com/complexity-5-interfaces" rel="alternate" type="text/html" title="Complexity part 5. Interfaces." /><published>2025-05-12T00:00:00+00:00</published><updated>2025-05-12T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-5-interfaces</id><content type="html" xml:base="http://dmtopolog.com/complexity-5-interfaces"><![CDATA[<p>Software is made of boundaries. From functions to classes, modules to services, systems to organizations — every meaningful split in a codebase depends on one thing: the interface.</p>

<p>An <strong>interface</strong> defines how two parts of a system interact. It tells you what you can rely on, what you need to provide, and what you can expect in return. At its simplest, it might be a function signature. At its most complex, it’s an entire service API, an SDK, or a protocol between two teams in different time zones.</p>

<p>We design interfaces constantly, often without calling them that:</p>

<ul>
  <li>Defining a function or data structure means deciding what’s public and what’s private.</li>
  <li>Splitting an app into modules shapes how features or services communicate.</li>
  <li>Connecting frontend and backend creates a contract for how data flows between them.</li>
  <li>Integrating third-party tools depends on remote APIs, SDKs, and configuration surfaces.</li>
  <li>UI components, design systems, and CLI tools all define interfaces for usage and composition.</li>
</ul>

<h2 id="why-interfaces-matter-for-complexity">Why Interfaces Matter for Complexity</h2>

<p>Interfaces are everywhere — some explicit, some implicit — and they shape how we write, test, extend, and reason about our systems. More than just technical artifacts like signatures or schemas, interfaces are how we <strong>hide complexity, enforce separation, and communicate intent</strong>. The complexity of a system doesn’t just come from how it works internally — it comes from <strong>how much of that internal mess others have to understand just to use it</strong>. Interfaces are the lever that controls that experience. They’re where complexity either gets absorbed — or spills over. Every time we define or consume an interface, we’re not just connecting code — we’re shaping how people think.</p>

<ul>
  <li>
    <p><strong>Interfaces determine coupling</strong><br />
Poorly abstracted interfaces often expose implementation details or force consumers to know too much. This tightens the coupling between systems and makes changes harder, riskier, and more expensive.</p>
  </li>
  <li>
    <p><strong>Interfaces create abstraction boundaries</strong><br />
They draw the line between what’s inside and what’s outside. A well-encapsulated interface reduces mental load and lets you work with a system without understanding its internals. A bad one forces you to think about too much at once.
<em>(<a href="/complexity-4-abstraction">More on abstraction</a>)</em></p>
  </li>
  <li>
    <p><strong>Interfaces act as contracts</strong><br />
They’re promises between parts of the system. The stronger and clearer the contract, the more confidently teams can work independently. Weak or unstable contracts breed uncertainty — and with it, defensive code, extra checks, and over-engineering.</p>
  </li>
  <li>
    <p><strong>Interfaces are where complexity compounds</strong><br />
Especially at team and system boundaries. Misalignment between frontend/backend, client/server, or feature/service often leads to glue code, mismatched assumptions, and integration bugs.</p>
  </li>
  <li>
    <p><strong>Interfaces shape how we use a system</strong><br />
They guide interactions and encode design intent. Clear, consistent interfaces make code easier to use and extend. Confusing ones lead to workarounds, forks, and rewrites.</p>
  </li>
</ul>

<h2 id="what-makes-an-interface-good-or-bad">What Makes an Interface Good or Bad?</h2>

<p>Not all interfaces are created equal. Some make systems easy to use, change, and reason about. Others create friction at every step. A good interface reduces complexity — a bad one amplifies it.</p>

<p>Here are some key properties that distinguish good interfaces from bad ones, along with examples that show how they play out in practice.</p>

<h3 id="1-clarity-vs-ambiguity">1. <strong>Clarity vs. Ambiguity</strong></h3>

<p><strong>Good interfaces</strong> are clear in intent and usage. You can tell what they do, what inputs they expect, and what outputs they produce — without reading the internal implementation.</p>

<p><strong>Bad interfaces</strong> are ambiguous. They require trial-and-error, reading the source, or asking someone else to understand how to use them correctly.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ <code class="language-plaintext highlighter-rouge">func sendEmail(to address: EmailAddress)</code> — clear intent.</li>
    <li>❌ <code class="language-plaintext highlighter-rouge">func process(_ input: Any)</code> — unclear purpose, undefined expectations.</li>
  </ul>
</blockquote>

<h3 id="2-minimal-surface-area-vs-overexposure">2. <strong>Minimal Surface Area vs. Overexposure</strong></h3>

<p><strong>Good interfaces</strong> expose just enough to do the job — and no more. They present a minimal, focused contract.</p>

<p><strong>Bad interfaces</strong> leak internal details or expose too many options “just in case,” increasing cognitive load and coupling.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ <code class="language-plaintext highlighter-rouge">analytics.track(event: .productViewed(page: .productPage, id: “123”))</code></li>
    <li>❌ <code class="language-plaintext highlighter-rouge">analytics.log(name: String, properties: [String: Any], sendImmediately: Bool)</code></li>
  </ul>
</blockquote>

<h3 id="3-consistency-vs-surprise">3. <strong>Consistency vs. Surprise</strong></h3>

<p><strong>Good interfaces</strong> behave consistently with the rest of the system. They follow naming, structure, and behavioral conventions that match other components.</p>

<p><strong>Bad interfaces</strong> break expectations, introduce some unintended side effects.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ Consistency across the similar functional objects
      <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">loginViewController</span> <span class="o">=</span> <span class="n">loginCoordinator</span><span class="o">.</span><span class="nf">start</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">productsViewController</span> <span class="o">=</span> <span class="n">productsCoordinator</span><span class="o">.</span><span class="nf">start</span><span class="p">()</span>
<span class="k">let</span> <span class="nv">profileViewController</span> <span class="o">=</span> <span class="n">profileCoordinator</span><span class="o">.</span><span class="nf">start</span><span class="p">()</span>
</code></pre></div>      </div>
    </li>
    <li>❌ Various design patterns across the similar objects
      <div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">loginRouter</span><span class="o">.</span><span class="nf">goToPasswordReset</span><span class="p">()</span>
<span class="n">products</span><span class="o">.</span><span class="nf">startFlow</span><span class="p">(</span><span class="k">in</span> <span class="nv">viewController</span><span class="p">:</span> <span class="n">parentViewController</span><span class="p">,</span>
                  <span class="nv">options</span><span class="p">:</span> <span class="p">[</span><span class="o">.</span><span class="nf">modalPresentationStyle</span><span class="p">(</span><span class="o">.</span><span class="n">fullScreen</span><span class="p">)])</span>
<span class="k">let</span> <span class="nv">profileViewController</span> <span class="o">=</span> <span class="n">profileCoordinator</span><span class="o">.</span><span class="nf">start</span><span class="p">()</span>
</code></pre></div>      </div>
    </li>
  </ul>
</blockquote>

<h3 id="4-stability-vs-volatility">4. <strong>Stability vs. Volatility</strong></h3>

<p><strong>Good interfaces</strong> are stable over time. They evolve carefully and intentionally.</p>

<p><strong>Bad interfaces</strong> change frequently — even in backwards-incompatible ways — forcing every dependent component to adapt.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ A versioned API with deprecated fields clearly marked.</li>
    <li>❌ A shared protocol that changes weekly, breaking multiple modules downstream.</li>
  </ul>
</blockquote>

<h3 id="5-encapsulation-vs-leakage">5. <strong>Encapsulation vs. Leakage</strong></h3>

<p><strong>Good interfaces</strong> hide complexity behind a clean surface.</p>

<p><strong>Bad interfaces</strong> force consumers to know too much about what’s behind the curtain — exposing implementation details, states, or dependencies that don’t belong outside.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ <code class="language-plaintext highlighter-rouge">Session.start()</code></li>
    <li>❌ <code class="language-plaintext highlighter-rouge">Session.prepare(userContext: UserContext, cache: CacheStore, options: [String: Any])</code></li>
  </ul>
</blockquote>

<h3 id="6-context-appropriate-vs-internally-driven">6. <strong>Context-Appropriate vs. Internally Driven</strong></h3>

<p><strong>Good interfaces</strong> reflect the mental model of the consumer. They speak the language of the problem domain, not the internal implementation.</p>

<p><strong>Bad interfaces</strong> mirror how the system is built rather than how it’s used — exposing low-level or irrelevant details to the caller.</p>

<blockquote>
  <p>🧩 <em>Example:</em></p>
  <ul>
    <li>✅ <code class="language-plaintext highlighter-rouge">Cart.totalPrice(in currency: CurrencyCode)</code></li>
    <li>❌ <code class="language-plaintext highlighter-rouge">Cart.getAmounts(applyTax: Bool, applyDiscount: Bool, useCachedRate: Bool)</code></li>
  </ul>
</blockquote>

<h3 id="7-focused-responsibility-vs-overgeneralization">7. <strong>Focused Responsibility vs. Overgeneralization</strong></h3>

<p><strong>Good interfaces</strong> do one thing well. They’re easy to explain in a sentence.</p>

<p><strong>Bad interfaces</strong> try to be everything to everyone — and end up doing none of it cleanly.</p>

<h3 id="8-ease-of-use-vs-defensive-usage">8. <strong>Ease of Use vs. Defensive Usage</strong></h3>

<p><strong>Good interfaces</strong> guide the user toward correct usage. They make the easy thing the right thing.</p>

<p><strong>Bad interfaces</strong> require defensive code, special flags, or knowledge of edge cases to avoid misuse.</p>

<hr />

<p>Good interfaces make code easier to read, use, change, and test. They absorb complexity — so it doesn’t spread. Bad interfaces do the opposite: they force complexity outward, making every consumer deal with it.</p>

<p>That’s why interface design isn’t just a technical detail. It’s one of the highest-leverage tools we have for managing complexity at scale.</p>

<h2 id="mitigating-complexity-with-better-interfaces">Mitigating Complexity with Better Interfaces</h2>

<p>Interfaces are one of the most powerful tools we have for managing complexity — but only if we design and evolve them with care. A poorly thought-out interface pushes complexity onto the user, while a good one hides it, clarifies intent, and enables safe, confident usage. Below are practical guidelines to help you shape interfaces that reduce friction, lower cognitive load, and evolve gracefully over time.</p>

<ul>
  <li>
    <p><strong>Keep them minimal</strong><br />
Expose only what’s necessary. The smaller the interface, the less there is to understand, misuse, or change. Simpler interfaces are easier to learn, test, and maintain.</p>
  </li>
  <li>
    <p><strong>Be consistent</strong><br />
Use consistent naming, parameter order, data shapes, and error handling across all your interfaces. Familiar, predictable patterns reduce mental effort and speed up comprehension.</p>
  </li>
  <li>
    <p><strong>Separate concerns</strong><br />
Avoid multipurpose interfaces. Each interface should have a focused responsibility — doing one thing well. Separation enables reuse without unintended coupling.</p>
  </li>
  <li>
    <p><strong>Invest in documentation</strong><br />
Choose clear, intention-revealing names, types, and constraints so the interface explains itself at a glance. Self-documentation helps, but it’s not enough — especially for shared or public interfaces. Clarify not just <em>what</em> the interface does, but <em>why</em> it exists and how it should be used.</p>
  </li>
  <li>
    <p><strong>Design for evolution</strong><br />
Interfaces tend to stick around. Design them with future growth in mind. Use techniques like deprecation, optional parameters, clear versioning, and compatibility layers to support change without disruption.</p>
  </li>
  <li>
    <p><strong>Use deep, not shallow modules</strong><br />
Favor interfaces that offer high value with minimal surface area. A deep module hides complexity behind a simple API (e.g., a <code class="language-plaintext highlighter-rouge">start()</code> method that kicks off an entire flow), while a shallow one forces the consumer to manage internals themselves.</p>
  </li>
  <li>
    <p><strong>Fail loudly and early</strong><br />
If an interface is used incorrectly, it should produce a clear error — not silently fail or allow invalid input to slip through. Defensive code at the boundary reduces bugs downstream.</p>
  </li>
  <li>
    <p><strong>Stabilize contracts</strong><br />
Once an interface is shared — especially across teams or services — treat it like a contract. Changing it carelessly creates ripple effects that increase complexity everywhere it’s used.</p>
  </li>
  <li>
    <p><strong>Design for the consumer, not the implementation</strong><br />
Structure the interface around how people use it — not how the system stores or calculates data. Consider the consumer’s mental model, what they know, and what will make their job easiest.</p>
  </li>
  <li>
    <p><strong>Create collaborative design processes</strong><br />
Interfaces shouldn’t be designed in isolation. Run interface or API reviews that include stakeholders from both sides of the boundary — those who build it and those who use it. This reduces the chance of costly mismatches.</p>
  </li>
  <li>
    <p><strong>Test interfaces from the outside</strong><br />
Use integration or contract testing to validate that your interfaces are usable and resilient. If a consumer test is hard to write or read, it may signal that the interface needs simplification.</p>
  </li>
</ul>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="complexity" /><category term="platform-agnostic" /><category term="no code" /><summary type="html"><![CDATA[Software is made of boundaries. From functions to classes, modules to services, systems to organizations — every meaningful split in a codebase depends on one thing: the interface.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-05-12-complexity-5-interfaces/header.jpg" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-05-12-complexity-5-interfaces/header.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 4. Abstractions.</title><link href="http://dmtopolog.com/complexity-4-abstraction" rel="alternate" type="text/html" title="Complexity part 4. Abstractions." /><published>2025-05-05T00:00:00+00:00</published><updated>2025-05-05T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-4-abstraction</id><content type="html" xml:base="http://dmtopolog.com/complexity-4-abstraction"><![CDATA[<p>In software development, abstraction is one of our most powerful tools — it allows us to hide irrelevant details and work with simplified mental models. It’s how we manage complexity: instead of thinking in terms of bits and bytes, we work with concepts like buttons, lists, users, or invoices. The goal of abstraction is to make systems easier to understand, use, and evolve by reducing the amount of information we need to hold in our heads at once.</p>

<p>But abstraction is also a double-edged sword. When done poorly — too early, too generically, or in the wrong context — it can increase complexity instead of reducing it. Layers of indirection, leaky contracts, mismatched models, and unnecessary generalization all make the system harder to reason about. This article explores how abstraction helps and harms, how to recognize bad abstractions, and how to build the right ones for the problems at hand.</p>

<h2 id="modeling-reality">Modeling Reality</h2>

<p>Abstractions are all around us — not just in software, but in how we perceive and understand the world. In the physical world, we work with layers of abstraction to make sense of complex systems. At the smallest scales, we study atoms, molecules, and cells. At higher levels, we talk about tissues, organs, organisms, and ecosystems. In physics, we might go from quantum particles, to materials, to human-scale mechanics, all the way up to planetary and galactic systems. We use different models for each of these layers because it would be overwhelming — and unnecessary — to consider all layers at once. We don’t need particle physics to treat a broken leg.</p>

<p>Abstractions are always adjusted to the <strong>context</strong>. Consider a country on a map: the same country can be represented in vastly different ways depending on what you’re trying to understand. A political map highlights borders and capitals. A population density map reveals demographic patterns. A topographic map shows terrain. All of these are abstractions of the same reality — but each is tuned for a specific purpose.</p>

<p>Software development follows the same pattern. When we build systems, we are modeling reality. Everything we create — data types, services, app modules, even full architectures — is a model of something. A <code class="language-plaintext highlighter-rouge">User</code> data type models a real person (or, more precisely, the aspects of that person that our system cares about). A <code class="language-plaintext highlighter-rouge">NotificationService</code> models the process of informing users. An entire app might model a shopping experience, a logistics pipeline, or a healthcare workflow.</p>

<p>These models, like all abstractions, intentionally leave out some details to make the system manageable. What we choose to represent — and what we choose to ignore — is guided by context.</p>

<p>Let’s say we work on a car simulation. We start with the simplest model: the car is just a point moving on a 1D grid. We don’t need more for the beginning. Then we develop our module and add movement controls — acceleration, braking — and simulate inertia. Then we work on the UI: put it to a 2D-space and add size to our moving point. Later, we want to make it even closer to reality, so we add visual components: shape, color, orientation. Eventually, we might simulate full 3D physics, engine temperature, tire wear, or even fuel type. Each version of the simulation is a valid model of a car adjusted to a specific context. As the context evolves the model changes as well.</p>

<p><img src="/images-posts/2025-05-05-complexity-4-abstraction/drawing-batman.jpg" alt="" /></p>

<p>The art of abstraction lies in making two essential decisions: first, choosing the right context — understanding what question we’re trying to answer and how we intend to work with the abstraction; and second, deciding which details are important enough to include and which can be safely left out, based on that chosen context.</p>

<p>When we get those two things right, abstraction reduces complexity. When we don’t, it often does the opposite.</p>

<p><img src="/images-posts/2025-05-05-complexity-4-abstraction/models-are-wrong.png" alt="" /></p>

<h2 id="common-abstraction-pitfalls">Common Abstraction Pitfalls</h2>

<p>Even though abstraction is a tool for managing complexity, it can easily become a source of complexity itself when misapplied. Here are some of the most common ways abstractions go wrong.</p>

<h3 id="1-wrong-level-of-abstraction">1. Wrong Level of Abstraction</h3>

<p>An abstraction that operates at the wrong level can either oversimplify or overcomplicate the problem. When it’s too shallow, it omits important details and fails to support real-world usage. When it’s too deep, it tries to cover every possible scenario — cluttering the interface with edge-case handling, parameters, and configuration options that most consumers don’t need.</p>

<p>This often stems from a desire to generalize too early — seeing two similar bits of logic and merging them into a shared abstraction. But surface-level similarity doesn’t always mean shared purpose. The result is a brittle abstraction that satisfies no use case fully, and becomes harder to evolve as requirements change.</p>

<p>You’ll often feel this pain when trying to extend the abstraction. Suddenly, the once-“generic” solution starts accumulating switches, conditionals, or awkward workarounds — all symptoms of a model that no longer fits.</p>

<h4 id="signs-this-is-happening">Signs this is happening:</h4>
<ul>
  <li>The abstraction claims to be general-purpose but only supports a subset of use cases in practice.</li>
  <li>It contains branching logic or edge-case handling that grows over time.</li>
  <li>It’s difficult to use correctly without reading the implementation.</li>
  <li>It feels like a compromise: not expressive enough for one case, too verbose for another.</li>
  <li>Every time a specific use case changes, you’re forced to touch the abstraction.</li>
  <li>You find yourself thinking: “This would be easier if each case had its own implementation.”</li>
  <li>You’re writing more code to support the abstraction than to solve the original problem.</li>
  <li>Refactoring it feels risky because it affects many unrelated parts of the codebase.</li>
</ul>

<h4 id="examples">Examples:</h4>
<ul>
  <li>A <code class="language-plaintext highlighter-rouge">Shape</code> protocol that assumes all shapes have corners — then you need to add a <code class="language-plaintext highlighter-rouge">Circle</code>, and everything breaks.</li>
  <li>A <code class="language-plaintext highlighter-rouge">BaseViewModel</code> with dozens of hooks to accommodate multiple screens, where each screen uses only a few of them — and often in incompatible ways.</li>
  <li>A <code class="language-plaintext highlighter-rouge">Config</code> object that tries to hold all environment settings, screen parameters, and user preferences in one place — but becomes unreadable and impossible to safely evolve.</li>
  <li>A <code class="language-plaintext highlighter-rouge">FormField</code> abstraction designed to cover every kind of input (text, number, date, dropdown, toggle), but the API becomes so bloated with configuration flags that each new field type needs multiple if-statements to function properly.</li>
</ul>

<blockquote>
  <p>✅ Hint: Before abstracting, ask: <strong>Are these really the same thing? Or are they just accidentally similar right now?</strong></p>
</blockquote>

<h3 id="2-redundant-abstraction">2. Redundant Abstraction</h3>

<p>There are many cases where an abstraction is created without a real need for one. This often happens in the form of <strong>premature abstraction</strong> — introducing layers before real duplication, variation, or friction appears. The intent may be to “do it right from the start” or to make future changes easier. But in reality, these abstractions are usually based on guesses — guesses about what might be reused, or what could change. The problem is, the future rarely plays out exactly as we expect.</p>

<p>Another common case is <strong>abstraction for the sake of “clean” architecture</strong>. We introduce protocols, wrappers, services, and layers to align with a favorite pattern or to satisfy a theoretical purity — not because the problem demands it. These layers create indirection, fragmentation, and mental overhead. Eventually, the cost of navigating the abstraction outweighs the benefits it provides.</p>

<h4 id="signs-this-is-happening-1">Signs this is happening:</h4>
<ul>
  <li>The abstraction is only used once — or has no meaningful reuse.</li>
  <li>It’s harder to onboard someone into the abstraction than to understand the actual logic.</li>
  <li>You use passthrough functions, parameters, or entire layers that don’t add value.</li>
  <li>You often bypass the abstraction or re-implement similar functionality elsewhere.</li>
  <li>You can’t explain why it exists without saying “just in case” or “it might be reused.”</li>
  <li>Understanding the code requires jumping between multiple files and types.</li>
  <li>The abstraction has more structure than substance — its logic is trivial or nonexistent.</li>
</ul>

<h4 id="examples-1">Examples:</h4>
<ul>
  <li>A <code class="language-plaintext highlighter-rouge">Logger</code> protocol introduced to allow log swapping — but no one ever uses any implementation other than <code class="language-plaintext highlighter-rouge">ConsoleLogger</code>.</li>
  <li>A <code class="language-plaintext highlighter-rouge">StorageManager</code> class that wraps <code class="language-plaintext highlighter-rouge">UserDefaults</code> with identical method signatures, adding no behavior beyond delegation.</li>
  <li>A <code class="language-plaintext highlighter-rouge">ViewModelProtocol</code> created for testability, but only one concrete implementation exists — and the test uses the real one anyway.</li>
  <li>A <code class="language-plaintext highlighter-rouge">NetworkClient</code> interface introduced early in the project, even though only one backend is used and no alternative is planned.</li>
  <li>A <code class="language-plaintext highlighter-rouge">Coordinator</code> protocol with no shared responsibilities, implemented by each screen with unrelated navigation logic.</li>
</ul>

<blockquote>
  <p>✅ Hint: Prefer concrete clarity over abstract purity.</p>
</blockquote>

<h3 id="3-leaky-abstractions">3. Leaky Abstractions</h3>

<p>A leaky abstraction is one that fails to fully hide the complexity it was meant to encapsulate. From the outside, the abstraction should offer a clean, predictable interface — shielding its users from internal details. But with a leaky abstraction, you find yourself digging into the implementation just to understand how to use it correctly or safely.</p>

<p>Joel Spolsky’s <em>Law of Leaky Abstractions</em> puts it clearly: <em>“All non-trivial abstractions, to some degree, are leaky.”</em> While some degree of leakage may be inevitable, it becomes a real problem when the abstraction <strong>pretends</strong> to simplify something but still forces you to understand what’s underneath. Instead of reducing cognitive load, it increases it — with a false sense of simplicity.</p>

<p>Leaky abstractions break encapsulation, increase coupling, hurt modularity, and frustrate developers trying to reason about the system.</p>

<h4 id="signs-this-is-happening-2">Signs this is happening:</h4>
<ul>
  <li>You need to understand internal behavior or edge cases to use the abstraction correctly.</li>
  <li>The abstraction exposes implementation-specific concepts in its API.</li>
  <li>You frequently open the source to “see what it’s doing under the hood.”</li>
  <li>You find yourself passing raw internals (like file paths, query strings, or framework-specific data) through an otherwise abstracted interface.</li>
  <li>You must remember undocumented caveats or usage patterns to avoid breaking things.</li>
  <li>You get unexpected bugs from valid usage — because <em>how</em> it works matters more than <em>what</em> it claims to do.</li>
  <li>The abstraction doesn’t match the mental model it tries to present.</li>
</ul>

<h4 id="examples-2">Examples:</h4>
<ul>
  <li>A <code class="language-plaintext highlighter-rouge">NavigationRouter</code> abstraction that wraps navigation logic but still requires you to consider whether you push to <code class="language-plaintext highlighter-rouge">NavigationController</code> or present something modally.</li>
  <li>An API client abstraction that handles requests (completely type-agnostic) but requires you to manually serialize and deserialize the payloads (considering exact response types with specific structure) — defeating the purpose of the abstraction.</li>
  <li>A <code class="language-plaintext highlighter-rouge">Theme</code> object that claims to encapsulate styles but exposes underlying UIKit values (like fonts and colors) without consistent mapping to the design system.</li>
  <li>A form validation abstraction that delegates to internal logic — but users must still know how validation errors are structured or displayed to use it effectively.</li>
</ul>

<h2 id="principles-for-better-abstractions">Principles for Better Abstractions</h2>

<p>Abstractions are not inherently good or bad — their quality depends on how well they fit the problem they’re trying to solve. When used appropriately, they reduce cognitive load, isolate complexity, and improve code reuse. When misused, they obscure logic, create fragility, and slow teams down. The key is to approach abstraction deliberately and iteratively — not dogmatically.</p>

<p>Here are some practical guidelines to help you design and evaluate abstractions more effectively:</p>

<ul>
  <li>
    <p><strong>Let abstractions emerge</strong><br />
Don’t invent abstractions too early. Let them arise naturally from repeated patterns or real-world friction. Extraction after the fact leads to better-aligned interfaces.</p>
  </li>
  <li>
    <p><strong>The name matters</strong><br />
A precise name forces clarity. If you struggle to name your abstraction, you probably haven’t figured out what it really does — and others will struggle to use it correctly.</p>
  </li>
  <li>
    <p><strong>Design for now, not for maybe</strong><br />
Build the abstraction that solves today’s problem well. Reuse should be a byproduct of clarity, not a justification for guessing what the future might need.</p>
  </li>
  <li>
    <p><strong>Hide what doesn’t need to be known</strong><br />
Keep the interface minimal. Expose only what’s essential, and shield consumers from irrelevant complexity.</p>
  </li>
  <li>
    <p><strong>Utilize the separation of concerns principle</strong><br />
If your abstraction is doing too much, it’s probably doing too little well. Split it into focused, single-purpose units.</p>
  </li>
  <li>
    <p><strong>Design by subtraction</strong><br />
Ask yourself: “What can I remove and still have this work?” Good abstractions tend to get simpler over time, not more complex.</p>
  </li>
  <li>
    <p><strong>Avoid passthrough layers</strong><br />
If an abstraction only delegates without adding meaning or behavior, remove it. Extra layers should earn their keep.</p>
  </li>
  <li>
    <p><strong>Reassess regularly</strong><br />
Abstractions that were once useful can become barriers. Revisit them when requirements change — don’t let outdated design decisions linger.</p>
  </li>
  <li>
    <p><strong>Don’t fear repetition</strong><br />
A little duplication is often better than the wrong abstraction. Try inlining and duplicating the logic — you might find it’s clearer. If not, re-extract with a better understanding.</p>
  </li>
  <li>
    <p><strong>If it leaks — rethink it</strong><br />
A leaky abstraction is often a sign of the wrong boundaries or misunderstood complexity. Either rework it, or eliminate it.</p>
  </li>
  <li>
    <p><strong>Document the purpose</strong><br />
Every abstraction should have a reason to exist. If you can’t explain it in one clear sentence, reconsider whether it’s needed at all.</p>
  </li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Abstractions are one of the most powerful ways we manage complexity in software projects. By hiding unnecessary details and organizing logic around meaningful concepts, they help us reason about systems more effectively and reduce the cognitive load required to work within them. <strong>Good abstractions do more than hide details — they shape how we think, debug, and build.</strong> They influence the mental models we form, the questions we ask, and the mistakes we avoid.</p>

<p>Like architecture, abstraction is not something we get right once — it’s something we adapt over time. The best abstractions are responsive, practical, and grounded in the real needs of the system. They reduce local complexity without introducing global confusion. When done well, abstraction becomes not just a technical device, but a strategic tool for building software that’s easier to understand, evolve, and trust.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="complexity" /><category term="platform-agnostic" /><category term="no code" /><summary type="html"><![CDATA[In software development, abstraction is one of our most powerful tools — it allows us to hide irrelevant details and work with simplified mental models. It’s how we manage complexity: instead of thinking in terms of bits and bytes, we work with concepts like buttons, lists, users, or invoices. The goal of abstraction is to make systems easier to understand, use, and evolve by reducing the amount of information we need to hold in our heads at once.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-05-05-complexity-4-abstraction/header.png" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-05-05-complexity-4-abstraction/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 3. Problem-solution mismatch</title><link href="http://dmtopolog.com/complexity-3-problem-solution-mismatch" rel="alternate" type="text/html" title="Complexity part 3. Problem-solution mismatch" /><published>2025-04-28T00:00:00+00:00</published><updated>2025-04-28T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-3-problem-solution-mismatch</id><content type="html" xml:base="http://dmtopolog.com/complexity-3-problem-solution-mismatch"><![CDATA[<p>In the previous chapters, we explored how <strong>low-level decisions</strong> and <strong>poor code organization</strong> contribute to growing complexity in software systems. These topics dealt mostly with <em>how</em> we write code. In this chapter, we take a step back to look at a higher-level cause of complexity — one that often lies deeper than syntax or structure: the <strong>misalignment between the problem and the solution</strong>.</p>

<p>When the solution doesn’t truly fit the problem it’s trying to solve, it introduces friction. Even if the code is well-written or follows best practices, if it doesn’t address the right shape of the problem, it becomes harder to understand, harder to maintain, and harder to evolve.</p>

<h3 id="causes-of-mismatch">Causes of Mismatch</h3>

<p>There are many ways a solution can become misaligned with the problem it’s supposed to address:</p>

<ul>
  <li>
    <p><strong>Suboptimal decisions at the start</strong><br />
We may choose an ill-fitting solution early on due to time pressure, incomplete information, or lack of experience.</p>
  </li>
  <li>
    <p><strong>The problem evolved, but the solution didn’t</strong><br />
A solution that once worked well may become awkward as requirements or context shift over time.</p>
  </li>
  <li>
    <p><strong>Overgeneralization or copy-pasting</strong><br />
We might borrow a pattern from another part of the app or from industry examples without fully understanding it. Similarity on the surface doesn’t mean the underlying problems are the same.</p>
  </li>
</ul>

<h3 id="signs-you-might-be-dealing-with-a-problemsolution-discrepancy">Signs You Might Be Dealing with a Problem–Solution Discrepancy</h3>

<p>Recognizing the mismatch is the first step. Here are some examples:</p>

<ul>
  <li>
    <p><strong>You have to frequently explain “why it’s done this way.”</strong><br />
When working with some piece of logic you need to gather some additional context. The design needs extensive documentation to justify itself.</p>
  </li>
  <li>
    <p><strong>You’re writing a lot of glue code, wrappers, or adapters.</strong><br />
These often appear when the parts don’t naturally fit together — you’re forcing compatibility where there isn’t any.</p>
  </li>
  <li>
    <p><strong>You’re breaking abstractions or layering rules.</strong><br />
For example, reaching deep into another layer’s details, using downcasting, adding one-off flags, or relying on fragile type checks — all signs that the abstraction isn’t holding up.</p>
  </li>
  <li>
    <p><strong>The code feels unintuitive or awkward.</strong><br />
Even if it “technically works,” it doesn’t feel natural or straightforward. You or your teammates might avoid touching it unless absolutely necessary.</p>
  </li>
</ul>

<h2 id="why-recognizing-mismatch-is-hard">Why Recognizing Mismatch Is Hard</h2>

<p>Recognizing a mismatch isn’t always easy. We tend to be biased toward what already exists (more about the biases in the following articles). Familiarity can be misleading — just because we understand a tool doesn’t mean it fits the problem. In high-pressure environments, we often optimize for short-term speed, making it easier to move forward with a known (but imperfect) solution than to pause and reconsider.</p>

<p>What makes this even harder is that problems often evolve gradually. We may not notice how much the current solution has drifted away from what the problem really requires — until the weight of complexity becomes too heavy to ignore.</p>

<h3 id="consequences-of-a-mismatch">Consequences of a Mismatch</h3>

<p><img src="/images-posts/2025-04-28-complexity-3-problem-solution-mismatch/cutting-with-spoon.png" alt="" /></p>

<p>Imagine you try to <strong>use a spoon to cut</strong> instead of a knife. Technically, it works — you can apply enough pressure and eventually get the job done. But it’s slow, awkward, and prone to mistakes. To make it more effective, you might start modifying the spoon, developing special techniques for using it, or building accessories to help stabilize what you’re cutting.</p>

<p>The more you try to improve the wrong tool, the more complexity you add. You end up with a mess of modifications, documentation, and rituals to support something that was never meant to do the job in the first place.</p>

<p>This is what happens when a solution doesn’t fit the problem: <strong>you pay an ongoing cognitive tax</strong>. The system becomes harder to understand, harder to explain, and harder to change.</p>

<h2 id="two-reactions-redesign-or-patch">Two Reactions: Redesign or Patch</h2>

<p>Once you recognize the mismatch, you’re usually left with two options. One is to <strong>redesign the solution from the ground up</strong> — to realign it with the actual shape of the problem. This is more difficult and time-consuming, but it typically results in a system that is simpler, more maintainable, and easier to work with in the long run.</p>

<p>The second, more common option is to <strong>patch the existing solution</strong>. We layer on exceptions, conditionals, adapters, and tweaks to make it “work.” But every patch is a small deviation from clarity, and over time, these patches accumulate. Eventually, the structure becomes difficult to reason about, and changes require more and more mental effort.</p>

<p>Although redesigning feels expensive upfront, it often saves time and complexity in the long term. In contrast, the patching approach is a form of <strong>debt</strong> — it accumulates quietly and compounds with every change.</p>

<h2 id="how-to-avoid-it">How to Avoid It</h2>

<ul>
  <li>
    <p><strong>Understand the problem deeply before jumping into the solution.</strong><br />
Rephrase it in multiple ways, sketch alternatives, and discuss with teammates. Don’t settle for the first idea that seems to work.</p>
  </li>
  <li>
    <p><strong>Start with the simplest viable shape.</strong><br />
Avoid overengineering or premature abstraction — you can always refactor once the problem is better understood.</p>
  </li>
  <li>
    <p><strong>Design by subtraction.</strong><br />
After your first draft, ask: <em>What can I remove without breaking the essence of the solution?</em></p>
  </li>
  <li>
    <p><strong>Be pragmatic — validate fit over elegance.</strong><br />
The best solution is not always the most elegant one. Sometimes the right shape is the boring one that just works.</p>
  </li>
  <li>
    <p><strong>Try to find several solutions.</strong><br />
Exploring a few directions allows you to compare trade-offs and better understand what matters most in your context.</p>
  </li>
  <li>
    <p><strong>Regularly revisit assumptions.</strong><br />
What worked before may not work now. Be willing to let go of solutions that no longer serve the problem.</p>
  </li>
</ul>

<hr />

<h2 id="agile-architecture">Agile architecture</h2>

<p>Misalignment doesn’t only affect individual pieces of logic — it can also affect the architecture of an entire module or even the whole app.</p>

<p>In modern development, we typically follow <strong>Agile</strong> methodologies. We start with a general idea and evolve the product incrementally, discovering the path as we go. This approach works well for products, but it also introduces a challenge: our architecture must adapt just as fluidly. If the system architecture is rigid or overly predetermined, it inevitably becomes <strong>misaligned</strong> with the evolving needs of the product.</p>

<p>That’s why architecture needs to be agile too. It must be designed with <strong>evolution in mind</strong>. This doesn’t mean skipping structure altogether, but rather <strong>building in flexibility</strong>: making it modular, keeping boundaries clean, favoring composition over inheritance, isolating decisions that are likely to change, and avoiding premature generalization.</p>

<p><img src="/images-posts/2025-04-28-complexity-3-problem-solution-mismatch/agile-architecture.png" alt="" /></p>

<p>Whether it’s called evolutionary architecture, emergent design, or just pragmatic refactoring — the principle is the same: <strong>keep your architecture open to change</strong>, so it can continue to match the problem it’s meant to solve. This alignment is what keeps complexity from spiraling out of control as your app grows.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="complexity" /><category term="platform-agnostic" /><category term="no code" /><summary type="html"><![CDATA[In the previous chapters, we explored how low-level decisions and poor code organization contribute to growing complexity in software systems. These topics dealt mostly with how we write code. In this chapter, we take a step back to look at a higher-level cause of complexity — one that often lies deeper than syntax or structure: the misalignment between the problem and the solution.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-04-28-complexity-3-problem-solution-mismatch/header.png" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-04-28-complexity-3-problem-solution-mismatch/header.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 2. Code and logic distribution.</title><link href="http://dmtopolog.com/complexity-2-logic-code-disctribution" rel="alternate" type="text/html" title="Complexity part 2. Code and logic distribution." /><published>2025-04-21T00:00:00+00:00</published><updated>2025-04-21T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-2-logic-code-disctribution</id><content type="html" xml:base="http://dmtopolog.com/complexity-2-logic-code-disctribution"><![CDATA[<p>How we <strong>distribute logic</strong> and <strong>structure code</strong> across a project plays a major role in how complex that project feels. Even well-written code can become hard to work with if it’s scattered, inconsistently organized, or buried in the wrong place.</p>

<p>Developers spend a large portion of their time reading, navigating, and understanding code — not writing it — so clear structure is essential. A thoughtful distribution of logic reduces the mental effort required to follow the flow of the app, understand responsibilities, and make safe changes. In short, good structure keeps cognitive load low and complexity in check.</p>

<h2 id="foundational-principles">Foundational Principles</h2>

<p>Clear structure helps reduce complexity — but knowing how to separate and organize code well is not always obvious. As projects grow, responsibilities blur, logic spreads, and it becomes harder to decide where things should live. Fortunately, there are a few foundational principles that serve as practical guidelines for distributing logic in ways that reduce cognitive load and improve maintainability. These principles can be grouped into three categories, each addressing a different aspect of structure: <strong>limiting responsibilities</strong>, <strong>deciding what belongs together or apart</strong>, and <strong>controlling how much code knows about other code</strong>. Together, they help us shape systems where each part is easier to understand, reason about, and evolve — the very qualities that keep complexity under control.</p>

<h3 id="1-limiting-responsibilities-what-should-this-code-be-responsible-for">1. Limiting Responsibilities: What should this code be responsible for?</h3>

<p><strong>Separation of Concerns (SoC)</strong> is about dividing the system into parts that focus on different areas — such as UI rendering, business logic, networking, or persistence. When different responsibilities are mixed together — for instance, a view controller that fetches data, parses it, formats it for display, updates the UI, and manages navigation — the result is code that’s harder to reason about, harder to change, and more error-prone. SoC helps reduce complexity by ensuring each part of the system has a clear and focused role.</p>

<p>The <strong>Single Responsibility Principle (SRP)</strong> takes this idea to a more granular level, guiding how we design individual types and functions. Each class or function should have one reason to change. This helps isolate concerns and reduces the risk of unintended side effects during refactoring.</p>

<p>In practice, we apply these principles at multiple levels of abstraction:</p>

<ul>
  <li>
    <p>At the <strong>app level</strong>, we separate responsibilities across feature modules or sections of the app. For example, the onboarding flow, user profile, and checkout process might each live in their own isolated modules with clear ownership and dedicated navigation flows.</p>
  </li>
  <li>At the <strong>functional layer</strong>, we move responsibilities into dedicated roles, for example:
    <ul>
      <li><em>View controllers</em> to handle UI rendering and user interaction.</li>
      <li><em>View models</em> - to manage presentation logic and data preparation.</li>
      <li><em>Coordinators</em> (or flow controllers) - to control navigation and screen transitions.</li>
      <li><em>Services</em> - to handle specific tasks like networking, analytics, persistence, or remote configuration.</li>
    </ul>
  </li>
  <li>At the <strong>function and data type level</strong>, we apply SRP by avoiding bloated types and breaking down logic into focused pieces:
    <ul>
      <li>A function that parses and validates input might delegate formatting to a separate utility.</li>
      <li>A data model might be accompanied by a display model that formats its data for presentation.</li>
      <li>An API client should only concern itself with networking — not with how the data will be shown or stored.</li>
    </ul>
  </li>
</ul>

<p>These decisions aren’t always black and white. There’s a trade-off between separating responsibilities and over-fragmenting the codebase. If separation is taken too far, you may end up with many tiny components that are hard to trace and understand in context — increasing rather than reducing cognitive load. The key is <strong>clarity</strong>: split responsibilities when doing so makes the system easier to understand, test, and evolve — not just to follow a rule. Thoughtful separation leads to a codebase where each part does one thing well and is easy to reason about in isolation.</p>

<hr />

<h3 id="2-what-belongs-together-which-pieces-of-logic-should-be-grouped-and-which-should-be-separated">2. What Belongs Together: Which pieces of logic should be grouped, and which should be separated?</h3>

<p>When making decisions about grouping or separating logic — whether it’s functions, data types, functional layers, or entire feature modules — a few helpful questions can guide you:</p>
<ul>
  <li>Do you mostly need to keep the parts together in mind?</li>
  <li>Are the parts normally used together?</li>
  <li>Is it hard to understand one without the other?</li>
  <li>Do they have shared state, dependency, or data model?</li>
  <li>Do they manipulate the same object or concept?</li>
  <li>Do they perform different operations?</li>
  <li>Do they have different levels of detail or abstraction?</li>
  <li>Do they have a semantic relationship (e.g., general vs. specialized behavior)?</li>
</ul>

<p>While responsibility defines <em>what</em> a piece of code should do, <strong>cohesion</strong> and <strong>coupling</strong> help us decide <em>how things should be grouped</em> and <em>how they should interact</em>. These principles are essential in shaping systems that are easy to navigate and evolve — they reduce the number of concepts a developer must juggle when working within a codebase.</p>

<p><strong>Cohesion</strong> refers to how closely related the functions and data within a module, type, or file are. High cohesion means everything inside serves a common purpose — making the module easier to understand and reason about. Low cohesion, by contrast, is a red flag: it suggests that unrelated logic has been lumped together, often out of convenience. For example, a <code class="language-plaintext highlighter-rouge">UserManager</code> that handles user authentication, profile formatting, local caching, and UI updates is trying to do too much. Splitting these into more focused units — like an <code class="language-plaintext highlighter-rouge">AuthService</code>, <code class="language-plaintext highlighter-rouge">UserProfileFormatter</code>, or <code class="language-plaintext highlighter-rouge">CacheManager</code> — improves clarity and testability.</p>

<p><strong>Coupling</strong> measures how dependent one piece of code is on another. Tight coupling means that changes in one component may require changes in another, which makes the system brittle and harder to maintain. Loose coupling enables components to evolve independently. For instance, if your view model is directly instantiating and managing its dependencies (e.g., networking code, data models), it becomes tightly coupled to them. Introducing abstractions like protocols and dependency injection reduces that coupling, allowing different parts of the codebase to be reused, tested, and replaced in isolation.</p>

<p>You can see these principles applied at multiple levels:</p>

<ul>
  <li>
    <p><strong>In feature modules</strong>: Grouping all the files related to a single feature — its views, view models, services, and navigation logic — into one place increases cohesion. This minimizes the mental jumps needed to understand or change a feature.</p>
  </li>
  <li>
    <p><strong>In types</strong>: A model object should only contain logic relevant to its role. Avoid putting formatting, validation, and networking logic into a single struct or class.</p>
  </li>
  <li>
    <p><strong>In functions</strong>: A function that handles user input should not also log analytics or trigger navigation. Keeping related behavior together, and unrelated behavior separate, leads to smaller, more focused functions that are easier to understand and reuse.</p>
  </li>
</ul>

<p>High cohesion and low coupling work together to reduce cognitive load. They localize logic, clarify intent, and limit the surface area affected by change — all of which make a codebase feel more navigable and less overwhelming.</p>

<hr />

<h3 id="3-limiting-knowledge-how-much-should-one-part-of-the-system-know-about-another">3. Limiting Knowledge: How much should one part of the system know about another?</h3>

<p>The third group of principles focuses on <strong>visibility and dependencies</strong> — in other words, how much knowledge one part of the system has about another. The less a component needs to know about the internals of others, the easier it is to understand and change in isolation. This group includes <strong>encapsulation</strong>, <strong>information hiding</strong>, and the <strong>Law of Demeter</strong>.</p>

<p><strong>Encapsulation</strong> is about hiding internal implementation details behind well-defined interfaces. When we encapsulate properly, other parts of the system don’t rely on the internal mechanics of a component — only on its public contract. This allows us to refactor the internal workings without breaking everything that depends on it. For instance, a <code class="language-plaintext highlighter-rouge">DataStore</code> might expose simple methods like <code class="language-plaintext highlighter-rouge">save()</code> and <code class="language-plaintext highlighter-rouge">load()</code> while hiding how the data is serialized or where it’s stored.</p>

<p><strong>Information hiding</strong> extends this idea to a broader scale. It’s the principle of <strong>not exposing more than what’s needed</strong>. A module shouldn’t leak internal types, intermediate states, or implementation-specific details unless absolutely necessary. This keeps boundaries clean and reduces the chance of other components forming hidden dependencies.</p>

<p>The <strong>Law of Demeter</strong> (“don’t talk to strangers”) advises that code should only interact with objects it directly owns or receives. This prevents “train wreck” code like <code class="language-plaintext highlighter-rouge">user.profile.settings.theme.name</code>, which exposes deep internal structures and creates tight coupling. Instead, higher-level objects should expose what’s needed explicitly. For example, <code class="language-plaintext highlighter-rouge">user.displayThemeName</code> is much easier to consume and hides the internal structure of the user profile.</p>

<p>These principles help reduce complexity by minimizing the amount of knowledge a developer needs to understand or safely modify a piece of code. When modules and types act as black boxes with clear inputs and outputs, it becomes easier to reason about behavior, write tests, and change internals without fear of unintended consequences.</p>

<p> </p>

<h2 id="applying-the-principles-in-practice-modularisation">Applying the Principles in Practice: Modularisation</h2>

<p>Splitting an application into multiple modules or components is one of the most effective ways to reduce complexity in larger projects. By breaking the system into <strong>logical units</strong> — such as <code class="language-plaintext highlighter-rouge">Authentication</code>, <code class="language-plaintext highlighter-rouge">Payments</code>, <code class="language-plaintext highlighter-rouge">UserProfile</code>, or <code class="language-plaintext highlighter-rouge">DesignSystem</code> — we create <strong>bounded contexts</strong> that are easier to understand, maintain, and evolve independently.</p>

<p>This practice directly applies principles like <strong>separation of concerns</strong> (each module handles its own domain), <strong>single responsibility</strong> (modules have one clear purpose), <strong>encapsulation</strong> (internal details are hidden behind module interfaces), and <strong>low coupling</strong> (modules depend only on well-defined contracts, not each other’s internals).</p>

<p>A well-modularized project allows teams to work in parallel, isolate bugs, write better-targeted tests, and reduce the mental overhead of navigating a massive codebase. Developers can reason about a feature in isolation, without needing to understand unrelated parts of the system. It also enables more controlled dependencies — for example, <code class="language-plaintext highlighter-rouge">UI</code> might depend on <code class="language-plaintext highlighter-rouge">DesignSystem</code>, but <code class="language-plaintext highlighter-rouge">DesignSystem</code> never depends on <code class="language-plaintext highlighter-rouge">UI</code>, keeping the dependency graph clean and directional.</p>

<p>Modularization doesn’t just reduce build times or improve testability — it helps <strong>shape the architecture</strong> in a way that reflects the real structure of the domain. This alignment lowers complexity and boosts confidence in the codebase. Also by applying decomposition we split one piece (an app or a feature module) into smaller and more digestable pieces.</p>

<p> </p>

<h2 id="applying-the-principles-in-practice-project-structure">Applying the Principles in Practice: Project structure</h2>

<p>The way we organize files and folders in a project is a direct reflection of the principles we’ve covered: <strong>separation of concerns</strong>, <strong>single responsibility</strong>, <strong>cohesion</strong>, <strong>coupling</strong>, and <strong>encapsulation</strong>. A thoughtful physical structure supports these ideas and helps reduce complexity by making the system easier to navigate and understand.</p>

<p>A common choice is between <strong>layer-based</strong> and <strong>feature-based</strong> structures. Layer-based organization groups files by type — for example, all views in one folder, all services in another. While this can work in simple projects, it often forces developers to jump across the codebase to trace a single feature. A <strong>feature-based</strong> structure, on the other hand, keeps related logic together — such as placing <code class="language-plaintext highlighter-rouge">ProfileView.swift</code>, <code class="language-plaintext highlighter-rouge">ProfileViewModel.swift</code>, and <code class="language-plaintext highlighter-rouge">ProfileService.swift</code> within a <code class="language-plaintext highlighter-rouge">Profile/</code> module. This approach increases cohesion and reduces the mental effort required to understand and modify a feature.</p>

<p>In Swift, it’s also important to be mindful of <strong>how we use extensions</strong>. It’s common to break a type into multiple files using extensions — for example: <code class="language-plaintext highlighter-rouge">User.swift</code> defines the core model, <code class="language-plaintext highlighter-rouge">User+Decoding.swift</code> adds conformance to <code class="language-plaintext highlighter-rouge">Decodable</code>, and <code class="language-plaintext highlighter-rouge">User+Display.swift</code> contains UI formatting logic. While this can improve clarity when used carefully, excessive fragmentation leads to poor cohesion and makes it harder to form a complete mental model of the type.</p>

<p>Files like <code class="language-plaintext highlighter-rouge">Utils.swift</code>, <code class="language-plaintext highlighter-rouge">Helpers.swift</code>, or <code class="language-plaintext highlighter-rouge">Extensions.swift</code> are another frequent sign of structural issues. These vaguely named containers often become dumping grounds for unrelated logic, violating principles like single responsibility and information hiding.</p>

<p>Ultimately, good physical structure aligns with how we think about the system. It helps developers find what they need, understand the scope of changes, and maintain the codebase with confidence.</p>

<p> </p>

<h2 id="applying-the-principles-in-practice-refactoring-a-function">Applying the Principles in Practice: Refactoring a function</h2>

<p>Here is the function that is triggered when a deep link arrives to the app. It’s quite verbose, so don’t dig into its details now.</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-1.png" alt="" /></p>

<p>Let’s see what the function does on a high level:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-2.png" alt="" /></p>

<p>Ideally these high-level operations should be the only things we see inside the function on this level of abstraction. The rest of the logic should be removed into the subfunctions. Let’s check then what is this logic in-between the high-level manipulation:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-3.png" alt="" /></p>

<p>The first part: <em>Manipulating the URL components</em> we can directly move to a subfunction:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-4.png" alt="" /></p>

<p>What are the exact operations we are doing here:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-5.png" alt="" /></p>

<p>Should we move these two operations to the next abstraction level and separate them into the subfunctions? Probably… But before doing this let’s check the object of manipulation within the fucntion:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-6.png" alt="" /></p>

<p>Even though we work with the query, path and host, technically, URLComponents is a shared state for these different operations. We manipulate the same object on the same level of abstraction, so if we split the function we will need to path the data in and out, that will complicate the understanding.</p>

<p>So we are leaving this <code class="language-plaintext highlighter-rouge">preprocessedURL()</code>-function as it is, without splitting it further. It is not tiny, but seems comprehensible enough.</p>

<p>Back to our root-function. We moved the preprocessing logic out. Should we do the same with another non-high-level part?</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-7.png" alt="" /></p>

<p>Let’s explore this option…</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-8.png" alt="" /></p>

<p>…looks neat.</p>

<p>And the last thing left is this tracking function:</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-9.png" alt="" /></p>

<p>It doesn’t belong to the high-level operations we perform here. Instead it’s a side-effect of opening a deep link. Every time we open a Deep Link we want to track it. So these 2 operations (passing a deep link further to open and tracking it) are related to the same event. So it makes sense to encapsulate this logic and separate it to a subfunction (that can be reused further if a deep link comes from somewhere other than the external URL: push notification, home screen widget or a Siri intent.):</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-10.png" alt="" /></p>

<p>Our root function became much simpler, more straightforward and comprehensible.</p>

<p><img src="/images-posts/2025-04-21-complexity-2-logic-code-disctribution/function-refactoring-11.png" alt="" /></p>

<p>We decomposed it, separated different level of abstractions, considered what belongs to each other and what doesn’t and even thought about some possible future improvements. Now it’s much easier to reason about this logic and if needed get deeper end explore the details.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Thoughtful code and logic distribution is one of the most effective ways to manage complexity in software projects. It shapes how easily developers can understand, navigate, and evolve a system. When structure reflects responsibility, cohesion keeps related pieces close, and visibility is properly constrained, the codebase becomes less like a maze and more like a well-marked map. The clearer the layout, the less mental effort is needed to get things done — and the more confident we can be when making changes. In the long run, it’s not just what the code does that matters, but how it’s organized — because structure is what makes complexity sustainable.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="complexity" /><category term="platform-agnostic" /><category term="code" /><summary type="html"><![CDATA[How we distribute logic and structure code across a project plays a major role in how complex that project feels. Even well-written code can become hard to work with if it’s scattered, inconsistently organized, or buried in the wrong place.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-04-21-complexity-2-logic-code-disctribution/nails-1920x620.png" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-04-21-complexity-2-logic-code-disctribution/nails-1920x620.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 1. Low-level decisions in code.</title><link href="http://dmtopolog.com/complexity-1-decisions-in-code" rel="alternate" type="text/html" title="Complexity part 1. Low-level decisions in code." /><published>2025-04-14T00:00:00+00:00</published><updated>2025-04-14T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-1-decisions-in-code</id><content type="html" xml:base="http://dmtopolog.com/complexity-1-decisions-in-code"><![CDATA[<p>In this piece, we focused on low-level decisions in code that quietly increase cognitive load: things like poor naming, deep nesting, magic values, etc. These choices often go unnoticed in the moment, but their impact adds up — making code harder to understand, maintain, and extend over time.</p>

<hr />

<p>When we think about complexity in software systems, our minds often jump to architecture diagrams, distributed systems, or multi-threaded concurrency nightmares. But in reality, complexity usually creeps in quietly, through small, seemingly insignificant choices: a deeply nested condition, a vague function name, a misused abstraction. It can lead to the <em>death by a thousand paper cuts</em> — no single decision sinks the system, but the accumulation wears it down, slows progress, and makes change harder than it should be.</p>

<h3 id="poor-naming">Poor Naming</h3>

<p>Naming is one of the most powerful tools we have as developers — and one of the easiest to misuse. When names are vague, overly generic, or misleading, they turn code into a puzzle. A variable called <code class="language-plaintext highlighter-rouge">data</code>, a function called <code class="language-plaintext highlighter-rouge">handle()</code>, or a class named <code class="language-plaintext highlighter-rouge">Manager</code> tells us almost nothing about what these things do or are. In mobile development, it’s common to stumble upon things like <code class="language-plaintext highlighter-rouge">InfoView</code>, <code class="language-plaintext highlighter-rouge">HelperClass</code>, or <code class="language-plaintext highlighter-rouge">process()</code> — labels that beg the question: what info? which helper? process what?</p>

<p>Poor naming forces readers to dig for context, scanning other parts of the code to figure out what something means. It increases the time it takes to understand the logic and decreases confidence when making changes. Worse, it creates a false sense of understanding — a name like <code class="language-plaintext highlighter-rouge">updateUI()</code> might sound harmless until you realize it’s triggering network calls and state changes under the hood.</p>

<p>Good names reveal intent. A function called <code class="language-plaintext highlighter-rouge">fetchUserProfile()</code> or a type called <code class="language-plaintext highlighter-rouge">OnboardingStepViewModel</code> communicates its purpose directly, without requiring comments or detective work. When names obscure the real purpose or behavior of code, it becomes harder to reason about the system as a whole. Understanding what each piece does, how it fits with others, or where responsibility lies becomes a constant cognitive burden. Over time, the system becomes fragile not because it’s technically flawed, but because it’s semantically incoherent. Clear, purposeful naming is the first step toward a codebase that’s easy to read, maintain, and grow.</p>

<h3 id="cyclomatic-complexity">Cyclomatic complexity</h3>

<p>High cyclomatic complexity is often a sign that a function is trying to do too much. It means the number of distinct paths through the code is high — often due to excessive branching with if, switch, guard, or complex nesting of conditions. While technically it might still “work,” reading it feels like navigating a maze. You can’t simply follow the logic top to bottom; instead, your mind has to simulate conditions, jump between branches, and keep a mental stack of possible outcomes.</p>

<p>A high-complexity function resists change because each small edit might ripple through a dozen logic paths. Bugs hide in obscure branches. Unit tests become hard to write or incomplete. The solution is not to avoid branching altogether, but to isolate it. Break down decision logic into smaller, testable functions. Favor early returns and reduce deep nesting. Refactor complex conditionals into named booleans or even strategy objects. The goal isn’t to chase a number — it’s to shape the code so that reasoning about it becomes linear again.</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">doSomething</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">10</span>
    <span class="k">if</span> <span class="n">y</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">y</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">0</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">doSomething</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">10</span>
    <span class="k">return</span> <span class="n">y</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Sometimes this complexity is <strong>essential</strong> (inherent to the problem), so we cannot do much with it. The 2 funtions in the previous example just do different things.</p>

<p>But in other cases we can do some changes to minimise cyclomatic complexity.</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">handleNotification</span><span class="p">(</span><span class="n">_</span> <span class="nv">type</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">isUserLoggedIn</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="s">"message"</span> <span class="p">{</span>                    
        <span class="k">if</span> <span class="n">isUserLoggedIn</span> <span class="p">{</span>
            <span class="nf">openMessages</span><span class="p">()</span>
        <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
            <span class="nf">showLoginPrompt</span><span class="p">()</span>
        <span class="p">}</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="s">"promo"</span> <span class="p">{</span>
        <span class="nf">showPromotion</span><span class="p">()</span>
    <span class="p">}</span> <span class="k">else</span> <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="s">"alert"</span> <span class="p">{</span>
        <span class="nf">showAlert</span><span class="p">()</span>
    <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>
<p>(cyclomatic complexity = 5)</p>

<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">handleNotification</span><span class="p">(</span><span class="n">_</span> <span class="nv">type</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> <span class="nv">isUserLoggedIn</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">switch</span> <span class="n">type</span> <span class="p">{</span>
    <span class="k">case</span> <span class="s">"message"</span><span class="p">:</span>
        <span class="nf">handleMessage</span><span class="p">(</span><span class="n">isUserLoggedIn</span><span class="p">)</span>
    <span class="k">case</span> <span class="s">"promo"</span><span class="p">:</span> 
        <span class="nf">showPromotion</span><span class="p">()</span>
    <span class="k">case</span> <span class="s">"alert"</span><span class="p">:</span>
        <span class="nf">showAlert</span><span class="p">()</span>
    <span class="k">default</span><span class="p">:</span>
        <span class="k">break</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">func</span> <span class="nf">handleMessage</span><span class="p">(</span><span class="n">_</span> <span class="nv">isUserLoggedIn</span><span class="p">:</span> <span class="kt">Bool</span><span class="p">)</span> <span class="p">{</span>
    <span class="n">isUserLoggedIn</span> <span class="p">?</span> <span class="nf">openMessages</span><span class="p">()</span> <span class="p">:</span> <span class="nf">showLoginPrompt</span><span class="p">()</span>
<span class="p">}</span>

</code></pre></div></div>
<p>(cyclomatic complexity = 3 + 2)</p>

<p>Even though the overall complexity of this piece of logic remained the same we split it into 2 functions and made it more digestable. As in most cases you don’t need to consume it all at once. Moreover we got a better separation of concerns; and 2 small functions are easier to test then one big one.</p>

<p>It’s usually considered that when a function has more than two or three returns, it’s a sign that the complexity is too high and refactoring is suggested. With complexity more than 10, a linter normally shows a red flag.</p>

<h3 id="nesting-level">Nesting level</h3>

<p>Nesting level is something we can fully control as developers.
In the following example we have 3 levels of nested types: PaymentConfiguration, Entry, and Content.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">PaymentConfiguration</span> <span class="p">{</span>
    
    <span class="kd">public</span> <span class="kd">struct</span> <span class="kt">Entry</span> <span class="p">{</span>
        
        <span class="kd">public</span> <span class="kd">enum</span> <span class="kt">Content</span> <span class="p">{</span>
            <span class="k">case</span> <span class="nf">modal</span><span class="p">(</span><span class="kt">PaymentContext</span><span class="p">)</span>
            <span class="k">case</span> <span class="nf">deepLink</span><span class="p">(</span><span class="kt">DeepLinkContext</span><span class="p">)</span>
        <span class="p">}</span>
                
        <span class="kd">public</span> <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
        <span class="kd">public</span> <span class="k">let</span> <span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span>
        
        <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span> 
                    <span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">self</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="n">title</span>
            <span class="k">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="n">content</span>
        <span class="p">}</span>
    <span class="p">}</span>
        
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">entries</span><span class="p">:</span> <span class="p">[</span><span class="kt">Entry</span><span class="p">]</span>
        
    <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">entries</span><span class="p">:</span> <span class="p">[</span><span class="kt">Entry</span><span class="p">])</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">entries</span> <span class="o">=</span> <span class="n">entries</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The cognitive load of such solution high as for each nested structure you need to keep the context of its parent (or even grandparent) in mind. You deal with the <code class="language-plaintext highlighter-rouge">Content</code>, but you will likely need to consider <code class="language-plaintext highlighter-rouge">Entry</code> and maybe even <code class="language-plaintext highlighter-rouge">PaymentConfiguration</code> for the full context. The cognitive contexts of them overlap:</p>

<p><img src="/images-posts/2025-04-14-complexity-1-decisions-in-code/context-overlap.png" alt="" /></p>

<p>So in this case it’s probably easier to split them into a separate data types, even if it looks more verbose in terms of naming.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">struct</span> <span class="kt">PaymentConfiguration</span> <span class="p">{</span>
        
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">entries</span><span class="p">:</span> <span class="p">[</span><span class="kt">PaymentConfigurationEntry</span><span class="p">]</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">entries</span><span class="p">:</span> <span class="p">[</span><span class="kt">PaymentConfigurationEntry</span><span class="p">])</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">entries</span> <span class="o">=</span> <span class="n">entries</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">struct</span> <span class="kt">PaymentConfigurationEntry</span> <span class="p">{</span>
            
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">title</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">content</span><span class="p">:</span> <span class="kt">PaymentConfigurationEntryEntryContent</span>
    <span class="kd">public</span> <span class="nf">init</span><span class="p">(</span><span class="nv">title</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
                <span class="nv">content</span><span class="p">:</span> <span class="kt">Content</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">self</span><span class="o">.</span><span class="n">title</span> <span class="o">=</span> <span class="n">title</span>
        <span class="k">self</span><span class="o">.</span><span class="n">content</span> <span class="o">=</span> <span class="n">content</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="kd">public</span> <span class="kd">enum</span> <span class="kt">PaymentConfigurationEntryEntryContent</span> <span class="p">{</span>
    <span class="k">case</span> <span class="nf">modal</span><span class="p">(</span><span class="kt">PaymentContext</span><span class="p">)</span>
    <span class="k">case</span> <span class="nf">confirmedDeepLink</span><span class="p">(</span><span class="kt">DeepLinkContext</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This way the contexts don’t overlap:</p>

<p><img src="/images-posts/2025-04-14-complexity-1-decisions-in-code/context-overlap-fixed.png" alt="" /></p>

<p>There might be some exception like using nested structs and enums as name spaces.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">enum</span> <span class="kt">AccessibilityIdentifiers</span> <span class="p">{</span>
    
    <span class="c1">// MARK: - AboutThisApp</span>
    
    <span class="kd">public</span> <span class="kd">enum</span> <span class="kt">About</span> <span class="p">{</span>
        <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">view</span> <span class="o">=</span> <span class="s">"view"</span>
        <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">list</span> <span class="o">=</span> <span class="s">"list"</span>
        <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">tour</span> <span class="o">=</span> <span class="s">"tour"</span>
        
        <span class="kd">public</span> <span class="kd">enum</span> <span class="kt">Legal</span> <span class="p">{</span>
            <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">view</span> <span class="o">=</span> <span class="s">"view"</span>
            <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">terms</span> <span class="o">=</span> <span class="s">"legal"</span>
            <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">copyrights</span> <span class="o">=</span> <span class="s">"copyrights"</span>
            
            <span class="kd">public</span> <span class="kd">enum</span> <span class="kt">Copyrights</span> <span class="p">{</span>
                <span class="kd">public</span> <span class="kd">static</span> <span class="k">var</span> <span class="nv">view</span> <span class="o">=</span> <span class="s">"view"</span>
                <span class="o">...</span>
            <span class="p">}</span>
            <span class="o">...</span>
        <span class="p">}</span>
        <span class="o">...</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So it can be used like <code class="language-plaintext highlighter-rouge">AccessibilityIdentifiers.About.Legal.copyrights</code>.</p>

<p>But be careful with it, it only works when you don’t need to get inside this nested types, which is the case for constants like Accessibility Identifiers. If the types require instantiation or even some logic (like the previous example with <code class="language-plaintext highlighter-rouge">AboutThisAppConfiguration</code>), you will need to ocasionally take a look inside it. Hence no nesting is suggested in those cases.</p>

<h3 id="number-of-parameters">Number of parameters</h3>

<p>Functions with too many parameters often signal a deeper design issue. They might be taking on multiple responsibilities, or they’re being forced to know too much about their environment. From a cognitive standpoint, each parameter is a fact the reader must keep in mind while understanding the function. As the number grows, it becomes harder to mentally simulate the behavior, trace data flow, or confidently make changes.</p>

<p>One way to address this is by grouping related parameters into meaningful data structures — for example, bundling latitude, longitude, and altitude into a Location object, or firstName, lastName, and email into a UserProfile. This not only simplifies the function signature, but also communicates the conceptual relationship between the parameters.</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="nf">trackEvent</span><span class="p">(</span><span class="nv">formId</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
                       <span class="nv">formStep</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
                       <span class="nv">formStatus</span><span class="p">:</span> <span class="kt">String</span><span class="p">,</span>
                       <span class="nv">transactionId</span><span class="p">:</span> <span class="kt">String</span><span class="p">?,</span>
                       <span class="nv">formOutcome</span><span class="p">:</span> <span class="kt">String</span><span class="p">?,</span>
                       <span class="nv">formType</span><span class="p">:</span> <span class="kt">String</span><span class="p">?)</span> <span class="p">{</span>
    <span class="o">...</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">func</span> <span class="nf">trackEvent</span><span class="p">(</span><span class="nv">content</span><span class="p">:</span> <span class="kt">EventContent</span><span class="p">)</span> <span class="p">{</span>
    <span class="o">...</span>
<span class="p">}</span>
    
<span class="kd">public</span> <span class="kd">struct</span> <span class="kt">EventContent</span><span class="p">:</span> <span class="kt">Equatable</span><span class="p">,</span> <span class="kt">Sendable</span> <span class="p">{</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">formId</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">formStep</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">formStatus</span><span class="p">:</span> <span class="kt">String</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">transactionId</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">formOutcome</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span>
    <span class="kd">public</span> <span class="k">let</span> <span class="nv">formType</span><span class="p">:</span> <span class="kt">String</span><span class="p">?</span>
<span class="p">}</span> 
</code></pre></div></div>

<p>This reduces the number of entities you need to keep in mind when working with the function.</p>

<p>In other cases, if a function is doing too much and therefore needs too much context, it might be time to split it into smaller, more focused pieces. Reducing parameter count improves readability, testability, and long-term maintainability.</p>

<h3 id="complex-conditions">Complex conditions</h3>

<p>Complex conditions may be completely unreadable:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">yourFeatureToggleState</span> <span class="o">==</span> <span class="o">.</span><span class="n">enabled</span> <span class="o">&amp;&amp;</span>
    <span class="p">(</span><span class="n">numberOfLaunches</span> <span class="o">&gt;=</span> <span class="n">configuration</span><span class="o">.</span><span class="n">minimumRequiredLaunches</span><span class="p">)</span> <span class="o">||</span>
    <span class="p">(</span><span class="n">currentDate</span> <span class="o">&gt;=</span> <span class="n">nextPromptDate</span> <span class="o">&amp;&amp;</span>
    <span class="n">applicationVersion</span> <span class="o">!=</span> <span class="n">latestVersionWithAlert</span><span class="p">))</span> <span class="p">{</span>
    <span class="nf">attemptToShowAlert</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">window</span><span class="p">,</span> <span class="nv">completion</span><span class="p">:</span> <span class="n">completion</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>A better approach: break them into logical steps, assign parts to descriptive boolean variables, use those variables to make the condition self-explanatory:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">featureIsActive</span> <span class="o">=</span> <span class="n">yourFeatureToggleState</span> <span class="o">==</span> <span class="o">.</span><span class="n">enabled</span>
<span class="k">let</span> <span class="nv">enoughLaunches</span> <span class="o">=</span> <span class="n">numberOfLaunches</span> <span class="o">&gt;=</span> <span class="n">configuration</span><span class="o">.</span><span class="n">minimumRequiredLaunches</span>
<span class="k">let</span> <span class="nv">dateVersionConditionMet</span> <span class="o">=</span> <span class="p">(</span><span class="n">currentDate</span> <span class="o">&gt;=</span> <span class="n">nextPromptDate</span> <span class="o">&amp;&amp;</span>
    <span class="n">applicationVersion</span> <span class="o">!=</span> <span class="n">latestVersionWithAlert</span><span class="p">)</span>


<span class="k">if</span> <span class="n">featureIsActive</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">enoughLaunches</span> <span class="o">||</span> <span class="n">dateVersionConditionMet</span><span class="p">)</span> <span class="p">{</span>
<span class="nf">attemptToShowNativeRating</span><span class="p">(</span><span class="nv">in</span><span class="p">:</span> <span class="n">windowScene</span><span class="p">,</span> <span class="nv">completion</span><span class="p">:</span> <span class="n">completion</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="nested-if">Nested <code class="language-plaintext highlighter-rouge">if</code></h3>

<p>Nested <code class="language-plaintext highlighter-rouge">if</code>-conditions (<em>which are a specific case of increasing cyclomatic complexity</em>) deserve a separate mention, as it’s one of the most widely spread anti-pattern. Usually you can quite easily simplify it:</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">cardIds</span><span class="o">.</span><span class="n">count</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="p">{</span>
    <span class="nf">cardDismissHandler</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">viewController</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
        <span class="nf">cardDismissHandler</span><span class="p">()</span>
    <span class="p">}</span>
    <span class="n">delegate</span><span class="p">?</span><span class="o">.</span><span class="nf">dismissViewController</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">location</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">cardIds</span><span class="o">.</span><span class="n">count</span> <span class="o">&gt;</span> <span class="mi">1</span> <span class="o">||</span> <span class="n">viewController</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span>
    <span class="nf">cardDismissHandler</span><span class="p">()</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">delegate</span><span class="p">?</span><span class="o">.</span><span class="nf">dismissViewController</span><span class="p">(</span><span class="nv">at</span><span class="p">:</span> <span class="n">location</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If your language supports early returns or constructs like guard, you can flatten the structure and improve readability:</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="k">let</span> <span class="nv">cardId</span> <span class="p">{</span>
    <span class="k">if</span> <span class="k">let</span> <span class="nv">element</span> <span class="o">=</span> <span class="n">interaction</span><span class="o">.</span><span class="nf">element</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">location</span><span class="p">,</span>
                                         <span class="nv">cardId</span><span class="p">:</span> <span class="n">cardId</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span>
    <span class="p">}</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
    <span class="n">tracker</span><span class="o">.</span><span class="nf">trackElements</span><span class="p">(</span><span class="n">element</span><span class="p">,</span>
                          <span class="nv">page</span><span class="p">:</span> <span class="nf">analyticsPage</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">productName</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>
<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">guard</span> <span class="k">let</span> <span class="nv">cardId</span><span class="p">,</span> <span class="k">let</span> <span class="nv">element</span> <span class="o">=</span> <span class="n">interaction</span><span class="o">.</span><span class="nf">element</span><span class="p">(</span><span class="nv">for</span><span class="p">:</span> <span class="n">insightsLocation</span><span class="p">,</span>
                                                    <span class="nv">cardId</span><span class="p">:</span> <span class="n">cardId</span><span class="p">)</span> <span class="k">else</span> <span class="p">{</span> 
    <span class="k">return</span> 
<span class="p">}</span>

<span class="n">tracker</span><span class="o">.</span><span class="nf">trackElements</span><span class="p">(</span><span class="n">element</span><span class="p">,</span> <span class="nv">page</span><span class="p">:</span> <span class="nf">analyticsPage</span><span class="p">(</span><span class="nv">with</span><span class="p">:</span> <span class="n">productName</span><span class="p">))</span>
</code></pre></div></div>

<h3 id="magic-numbers-and-strings">Magic Numbers and Strings</h3>

<p><strong>Magic numbers and strings</strong> refer to hardcoded values that are used directly in the code without any explanation of their meaning or purpose. These “magic” values can significantly increase cognitive load, as developers must either infer their purpose from context or search through the codebase to understand what they represent. This makes the code harder to maintain and extend because any changes to these values require updating them in multiple places, increasing the risk of errors. Using <strong>descriptive constants</strong> instead of magic numbers or strings makes the code more readable and maintainable, as the intent behind the values is clearer and any future changes can be made in one central location.</p>

<p><em>More complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">calculateDiscount</span><span class="p">(</span><span class="nv">price</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Double</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">price</span> <span class="o">&gt;</span> <span class="mf">100.0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">price</span> <span class="o">*</span> <span class="mf">0.1</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">price</span> <span class="o">*</span> <span class="mf">0.05</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In the above example, <code class="language-plaintext highlighter-rouge">100.0</code> and <code class="language-plaintext highlighter-rouge">0.1</code> are magic numbers. It’s unclear why 100 is the threshold for a discount or what the significance of <code class="language-plaintext highlighter-rouge">0.1</code> is.</p>

<p><em>Less complex:</em></p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">let</span> <span class="nv">DISCOUNT_THRESHOLD</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">100.0</span>
<span class="k">let</span> <span class="nv">STANDARD_DISCOUNT_RATE</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">0.05</span>
<span class="k">let</span> <span class="nv">PREMIUM_DISCOUNT_RATE</span><span class="p">:</span> <span class="kt">Double</span> <span class="o">=</span> <span class="mf">0.1</span>

<span class="kd">func</span> <span class="nf">calculateDiscount</span><span class="p">(</span><span class="nv">price</span><span class="p">:</span> <span class="kt">Double</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Double</span> <span class="p">{</span>
    <span class="k">if</span> <span class="n">price</span> <span class="o">&gt;</span> <span class="kt">DISCOUNT_THRESHOLD</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">price</span> <span class="o">*</span> <span class="kt">PREMIUM_DISCOUNT_RATE</span>
    <span class="p">}</span>
    <span class="k">return</span> <span class="n">price</span> <span class="o">*</span> <span class="kt">STANDARD_DISCOUNT_RATE</span>
<span class="p">}</span>
</code></pre></div></div>

<p>By using constants with descriptive names, the intent of the values becomes much clearer, reducing complexity and making the code easier to understand and maintain.</p>

<h3 id="implicit-context-or-hidden-state">Implicit Context or Hidden State</h3>

<p>Code that relies on implicit context — such as global variables, singleton instances, or shared mutable state — quickly becomes difficult to reason about. When a function depends on or mutates state that isn’t passed in explicitly, it creates invisible dependencies. From the outside, it’s unclear what the function needs to operate correctly or how it might change the state of the system. This makes the code fragile: changing one piece can unintentionally break another, and understanding a function in isolation becomes almost impossible.</p>

<p>This kind of hidden complexity increases cognitive load because it forces developers to mentally reconstruct the larger system state just to understand a small piece of behavior. For example, a <code class="language-plaintext highlighter-rouge">UserSessionManager</code> that reads the current user from a global <code class="language-plaintext highlighter-rouge">AuthContext</code> may seem convenient, but any changes to the context mechanism or its timing can have unpredictable ripple effects. A better approach is to pass the necessary data explicitly and return any results or changes transparently. This not only makes individual components easier to understand and test, but also encourages better separation of concerns and modular design.</p>

<h3 id="summary-the-accumulated-weight-of-small-decisions">Summary: The Accumulated Weight of Small Decisions</h3>

<p>In this section, we explored how seemingly small, low-level decisions in code can significantly increase the cognitive load on developers. These patterns don’t usually break a system outright, but they do make the code harder to read, reason about, and modify. The more of them you allow to creep in, the more they compound, gradually turning simple features into fragile puzzles.</p>

<p>This additive nature of complexity is what makes these small decisions matter. While any one of them might seem harmless, their combined effect is felt most painfully during maintenance or when new features need to be added. Codebases don’t become hard to work with overnight — they erode slowly, one decision at a time.</p>

<p>And this is just the surface. Classic books like <em>Clean Code</em> (Robert C. Martin), <em>Code Complete</em> (Steve McConnell), and <em>Refactoring</em> (Martin Fowler) dedicate entire chapters to these and many other micro-decisions that influence clarity, maintainability, and quality. They’re excellent resources if you want to dive deeper into best practices and learn how to spot and improve problematic code patterns.</p>

<p>As a guiding principle, always remember: <strong>code is written for humans first, and machines second</strong>. Compilers are fine with cryptic, tangled code — your teammates (and your future self) are not. Writing clean, understandable code is an investment in long-term productivity and peace of mind.</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="complexity" /><category term="platform-agnostic" /><category term="code" /><summary type="html"><![CDATA[In this piece, we focused on low-level decisions in code that quietly increase cognitive load: things like poor naming, deep nesting, magic values, etc. These choices often go unnoticed in the moment, but their impact adds up — making code harder to understand, maintain, and extend over time.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-04-14-complexity-1-decisions-in-code/header-1500.png" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-04-14-complexity-1-decisions-in-code/header-1500.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Complexity part 0. Introduction.</title><link href="http://dmtopolog.com/complexity-0-introduction" rel="alternate" type="text/html" title="Complexity part 0. Introduction." /><published>2025-04-07T00:00:00+00:00</published><updated>2025-04-07T00:00:00+00:00</updated><id>http://dmtopolog.com/complexity-0-introduction</id><content type="html" xml:base="http://dmtopolog.com/complexity-0-introduction"><![CDATA[<p>This is the introduction for the <em>Complexity series</em> - a set of articles where we will explore the causes of complexity in our projects (not only related to the code) and the ways to minimise it. In this piece we will touch upon what exactly <strong>complexity</strong> is, how can we measure it and why should we care about it at all. In the</p>

<p>The topic is mostly relevant for ones who spend decent amount of work time reading and maintaining existing code. I know there are developers and teams who produce and ship projects without maintaining them afterward. For them the problem is not so relevant I believe.</p>

<h2 id="what-is-complexity">What is Complexity?</h2>

<p>Complexity is not an absolute measure—it exists on a relative scale. One idea, system, or solution is more or less complex compared to another. The key factor that defines this scale is cognitive load—the mental effort required to understand and work with a concept, system, or solution.</p>

<p>When we describe something as “too complex,” we’re often expressing that we can’t comfortably hold all the moving parts in our head at once. This isn’t a failure of intelligence—it’s a reflection of the natural limits of human cognition.</p>

<p>To understand these limits, it helps to know how our brain processes information. This involves three core systems:</p>

<ul>
  <li><strong>Sensory memory</strong>, which quickly absorbs information from our environment (like reading code on a screen or hearing someone explain an algorithm).</li>
  <li><strong>Working memory</strong>, the active space where we process, evaluate, and manipulate information.</li>
  <li><strong>Long-term memory</strong>, which stores everything we’ve learned and can be retrieved when needed.</li>
</ul>

<p><img src="/images-posts/2025-04-07-complexity-0-introduction/memory.png" alt="" /></p>

<p>When we solve problems, design systems, or debug code, we’re mostly operating in working memory. It’s where the mental “action” happens.</p>

<p>But here’s the catch: working memory is extremely limited.</p>

<p>According to cognitive science, most people can actively manage only about 5 to 9 chunks of information at a time (a concept famously explored in <a href="https://en.wikipedia.org/wiki/The_Magical_Number_Seven,_Plus_or_Minus_Two">Miller’s Law, 1956</a>). A “chunk” might be a variable name, a function’s purpose, a dependency between modules, or a user’s intent.</p>

<p>If the code or system we’re working with requires us to hold 15 or 20 different things in mind—branches, side effects, state changes, API constraints—we quickly exceed our cognitive capacity. This is when we start to feel overwhelmed, miss important details, or make mistakes.</p>

<h2 id="types-of-complexity">Types of complexity</h2>

<p>When it comes to human-made systems, there are two types of complexity that are always present in some combination. Some systems are inherently difficult. So difficult that even the interface might look very complex. Take an aircraft cockpit, for example.</p>

<p><img src="/images-posts/2025-04-07-complexity-0-introduction/cockpit-2.jpg" alt="" /></p>

<p>There are decades of evolution behind this interface and it is already as easy as it can physically get. We cannot make it simpler without loosing some functionality. This type of complexity is called <strong>essential complexity</strong>.</p>

<p>In other cases, the system (for instance a device to make coffee) may be implemented differently.</p>

<p><img src="/images-posts/2025-04-07-complexity-0-introduction/coffee-devices.png" alt="" /></p>

<p>Both of these options bring the same amount of essential complexity. But the second one is much harder to use despite doing the same thing. This extra complexity (of the second approach compared to the first one) is called <strong>added</strong> or <strong>accidental</strong>.</p>

<p><img src="/images-posts/2025-04-07-complexity-0-introduction/essential-accidental.png" alt="" /></p>

<h2 id="what-is-cognitive-load-really">What Is Cognitive Load, Really?</h2>

<p>Cognitive load comes in three forms:</p>

<ul>
  <li><strong>Intrinsic load</strong> – The inherent complexity of the task itself.</li>
  <li><strong>Extraneous load</strong> – The added complexity introduced by poor design, confusing code, or unclear naming.</li>
  <li><strong>Germane load</strong> – The effort dedicated to learning, forming mental models, and gaining insight.</li>
</ul>

<p>Good software design doesn’t aim to eliminate cognitive load — it aims to reduce extraneous load (as we cannot do much with inherent complexity) and support germane load.</p>

<p>That means writing code and building systems that:</p>

<ul>
  <li>Clearly reflect intent</li>
  <li>Reduce unnecessary nesting or state juggling</li>
  <li>Use consistent patterns and naming</li>
  <li>Separate concerns logically</li>
  <li>…and other things we will discuss in details in the following articles.</li>
</ul>

<p>In essence: we can’t increase human cognitive capacity, but we can reduce how much of it our code consumes.</p>

<h2 id="why-does-complexity-matter">Why Does Complexity Matter?</h2>

<p>By now, it should be clear: the more complex a system is, the more cognitive capacity it demands — and the harder it becomes to work with. This isn’t just a matter of elegance or personal preference; it affects the very foundations of how we build, scale, and sustain software.</p>

<p>In software engineering, excessive cognitive load has a major impact:</p>

<ul>
  <li>
    <p><strong>Complex systems are harder to understand, debug, modify, and maintain.</strong> Every added layer of logic or abstraction increases the mental effort required to trace behavior and reason about change. This slows down development and makes even small changes risky.</p>
  </li>
  <li>
    <p><strong>The more time new developers spend grasping the codebase, the longer it takes for them to contribute.</strong> Onboarding becomes a bottleneck when the mental model of the system is hard to build. Instead of learning the business logic, newcomers spend their energy deciphering internal complexity.</p>
  </li>
  <li>
    <p><strong>Scaling is easier with simpler systems.</strong> Simple, modular designs can be extended with minimal side effects. In contrast, complex systems resist change—every addition feels like a Jenga move that could topple the whole thing.</p>
  </li>
  <li>
    <p><strong>Complex relationships between parts and hidden dependencies lead to unpredictable side effects and bugs.</strong> When components are tightly coupled or behavior is implicit, it’s easy to break one thing while changing another. These bugs are often subtle, hard to detect, and expensive to fix.</p>
  </li>
  <li>
    <p><strong>Decision-making is slower when we don’t fully understand the system.</strong> Whether it’s choosing where to place new code or deciding how to fix a bug, unclear systems create hesitation. Developers are more likely to delay action, over-engineer solutions, or second-guess themselves.</p>
  </li>
  <li>
    <p><strong>Communication speed depends on complexity.</strong> Complex systems require complex explanations, which leads to more meetings, more documentation, and more back-and-forth. Teams must spend more time in discussions; developers need to explain more to their non-technical colleagues. This communication overhead reduces focus time and slows down collaboration.</p>
  </li>
  <li>
    <p><strong>Worse developers’ well-being.</strong> Cognitive overload is one of the leading causes of frustration and burnout. Developers feel drained from the difficulty of the problems as well as from the difficulty of the tools and systems they’re forced to work with. Instead of engaging in creative problem-solving, they’re stuck untangling complexity.</p>
  </li>
</ul>

<p>That’s why we should care deeply about complexity—and strive to minimize the cognitive load of our solutions. Clear, maintainable systems don’t just improve productivity; they make the work more humane, sustainable, and rewarding.</p>

<h2 id="measuring-complexity">Measuring complexity.</h2>

<p>How can we measure something as abstract as complexity?</p>

<p>Even in the early days of programming, developers sought ways to do it.</p>

<h4 id="1-lines-of-code">1. Lines of code</h4>

<p>Counting lines of code is the simplest and oldest approach. It gives a rough sense of the size of a codebase or function. While more lines often correlate with greater complexity, it doesn’t capture how those lines behave—100 lines of well-structured logic may be easier to work with than 20 lines of tangled conditions. Still, this metric is useful for quick comparisons or spotting bloated methods.</p>

<h4 id="2-cyclomatic-complexity">2. Cyclomatic Complexity</h4>

<p>In the 1970s, a more refined metric emerged: Cyclomatic Complexity—which counts the number of independent paths through a block of code. Each conditional or loop adds a new decision path, increasing the effort required to test, understand, and reason about the code.</p>

<p>For example, this simple function has only one possible path, so its complexity is 1:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">doSomething</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">10</span>
    <span class="k">return</span> <span class="n">y</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Introduce a conditional statement, and it jumps to 2:</p>
<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">doSomething</span><span class="p">(</span><span class="nv">x</span><span class="p">:</span> <span class="kt">Int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Int</span> <span class="p">{</span>
    <span class="k">let</span> <span class="nv">y</span> <span class="o">=</span> <span class="n">x</span> <span class="o">+</span> <span class="mi">10</span>
    <span class="k">if</span> <span class="n">y</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">y</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">0</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>Loops and additional conditionals further increase complexity, requiring more unit tests and increasing the chance of bugs hiding in untested paths.</p>

<h4 id="3-halstead-volume">3. Halstead Volume</h4>

<p>The Halstead Volume metric calculates complexity based on the number of operators and operands in the code. It aims to reflect the mental effort required to understand the code’s structure, not just its control flow. The more unique elements and the more frequently they’re used, the higher the volume—and the higher the cognitive load. It’s especially useful in analyzing how dense or abstract code is, even if the flow is relatively linear.</p>

<h4 id="4-maximum-nesting-level">4. Maximum Nesting Level</h4>

<p>This metric tracks how deeply control structures (like if, for, while, etc.) are nested within each other. High nesting levels often signal increased cognitive load, as the reader must keep track of multiple levels of logic simultaneously. Deep nesting also makes it harder to isolate bugs, refactor code, or trace execution paths. Flattening nested logic typically improves readability and testability.</p>

<h4 id="5-number-of-parameters">5. Number of Parameters</h4>

<p>The more parameters a function takes, the more information the developer must supply and keep in mind. Large parameter lists often indicate a lack of clear abstraction or an overloaded responsibility. Ideally, functions should take only the data they truly need—grouping related values into structs or objects where appropriate. Fewer parameters usually mean simpler, more focused code.</p>

<h4 id="6-maintainability-index">6. Maintainability Index</h4>

<p>The Maintainability Index combines multiple metrics — Lines of Code, Cyclomatic Complexity, and Halstead Volume — into a single score from 0 to 100. A higher score suggests that the code is easier to maintain, while lower scores indicate more technical debt and higher risk. It’s useful as a broad health check across a codebase, though it should always be interpreted in context.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>MI = MAX(0,(171 - 
     5.2 * ln(Halstead Volume) - 
     0.23 * (Cyclomatic Complexity) - 
     16.2 * ln(Lines of Code)) * 
       100 / 171)
</code></pre></div></div>
<p>A function may pass this metric numerically but still be hard to understand due to naming, coupling, or domain complexity — so always pair it with qualitative judgment.</p>

<p> </p>

<p>Some metrics focus on data structures and object-oriented design:</p>

<h4 id="7-cohesion">7. Cohesion</h4>

<p>Cohesion measures how closely related a class or module’s internal responsibilities are. High cohesion means the parts work together toward a single purpose, making the code easier to understand and reuse. Low cohesion suggests the class is doing too much—or things that don’t logically belong together—indicating a candidate for refactoring. High cohesion leads to better modularity and lower cognitive load.</p>

<p>One common metric is LCOM (Lack of Cohesion of Methods), which analyzes how many methods in a class access the same fields. If most methods operate on distinct sets of fields, cohesion is low. If they operate on shared fields, cohesion is high.</p>

<h4 id="8-coupling">8. Coupling</h4>

<p>Coupling is the degree of interdependence between modules or components. Tight coupling means changes in one module often require changes in others, making the system harder to evolve. Loose (or low) coupling allows components to be developed, tested, and reused independently. Reducing coupling makes codebases more flexible and maintainable in the long run.</p>

<p>To measure it count the number of external classes or modules a given class references (via field types, method parameters, return types, method calls). A popular metric is CBO (Coupling Between Objects).</p>

<h4 id="9-depth-of-inheritance-tree-dit">9. Depth of Inheritance Tree (DIT)</h4>

<p>This metric indicates how many levels a class is from the root of the hierarchy. Deeper trees can make it harder to trace behavior, especially if multiple layers override the same methods. While inheritance can promote reuse, overusing it leads to fragile designs and unexpected interactions. Favoring composition over inheritance often reduces complexity here.</p>

<p>To measure it simply count the number of levels from the class to the root of the inheritance hierarchy.
E.g., if <code class="language-plaintext highlighter-rouge">ViewController → BaseController → UIViewController</code>, then DIT = 2.</p>

<h4 id="10-response-for-a-class-rfc">10. Response for a Class (RFC)</h4>

<p>Response for a Class measures the number of different methods that can be invoked in response to a message sent to an object. A high RFC suggests the class has many responsibilities or exposes too much internal behavior. This can overwhelm the reader and create tight coupling between the class and its clients. Lower RFC typically implies a cleaner, more focused interface.</p>

<p>To measure it count the class’s own methods and any other methods they call (including internal and external calls).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RFC = Number of methods defined in the class + number of methods those methods invoke
</code></pre></div></div>

<h4 id="11-number-of-own-methods">11. Number of Own Methods</h4>

<p>This metric counts how many methods are defined directly in the class. While not all methods are created equal, a high number can indicate that the class has too many responsibilities or behaviors. Keeping classes small and focused — ideally centered around a single responsibility — reduces the burden on readers and improves reusability.</p>

<p>To measure it count all the methods defined in the class file itself, excluding inherited ones. This includes instance methods, class methods, and private/internal methods. A high count often signals a violation of the Single Responsibility Principle</p>

<h4 id="12-number-of-overridden-methods">12. Number of Overridden Methods</h4>

<p>Tracking overridden methods helps identify cases where subclass behavior diverges from its parent. A high number may suggest that the base class isn’t providing a stable abstraction or that inheritance is being misused. When overriding becomes the norm rather than the exception, it’s often a sign that composition would be a better fit.</p>

<p>Count methods in the class that explicitly use the override keyword (in Swift or Java), or that match inherited method signatures. High counts may indicate unstable inheritance hierarchies or improper use of inheritance instead of composition.</p>

<h4 id="13-number-of-implemented-interfaces-noii">13. Number of Implemented Interfaces (NOII)</h4>

<p>This measures how many interfaces a class implements. While implementing interfaces supports flexibility and polymorphism, too many interfaces can dilute a class’s identity and increase its obligations. Excessive interface usage may lead to complex dependency graphs and make the class harder to test, understand, or change.</p>

<p> </p>

<h3 id="why-to-bother-measuring">Why to bother measuring?</h3>

<p>All these metrics are based on static code analysis. That’s what the tools like SonarQube, Checkmarx, and SwiftLint use to assess code quality.
Complexity in software development extends far beyond the code itself—we’ll explore that later. Still, poorly implemented abstract concepts will eventually lead to complex code, which we can measure using these techniques.</p>

<p><img src="/images-posts/2025-04-07-complexity-0-introduction/measure-to-manage.png" alt="" /></p>

<p>There are arguments regarding the origin of this idea, but it doesn’t really matter. Measuring doesn’t necessarily mean controlling, but it’s the first step toward it.</p>

<p> </p>

<p>That was it for now, to be continued…</p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="architecture" /><category term="no code" /><category term="complexity" /><category term="platform-agnostic" /><summary type="html"><![CDATA[This is the introduction for the Complexity series - a set of articles where we will explore the causes of complexity in our projects (not only related to the code) and the ways to minimise it. In this piece we will touch upon what exactly complexity is, how can we measure it and why should we care about it at all. In the]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2025-04-07-complexity-0-introduction/nails-1920x620.png" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2025-04-07-complexity-0-introduction/nails-1920x620.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Shift in the protocol paradigm</title><link href="http://dmtopolog.com/protocol-paradigm-shift/" rel="alternate" type="text/html" title="Shift in the protocol paradigm" /><published>2023-01-10T00:00:00+00:00</published><updated>2023-01-10T00:00:00+00:00</updated><id>http://dmtopolog.com/protocol-paradigm-shift</id><content type="html" xml:base="http://dmtopolog.com/protocol-paradigm-shift/"><![CDATA[<p>In this article we will be talking about the recent changes related to protocols: opaque, existential and generic types; <code class="language-plaintext highlighter-rouge">some</code> and <code class="language-plaintext highlighter-rouge">any</code>; runtime and compile-time polymorphism.</p>

<p>I won’t describe the details of those features, but will share my thoughts on how they change the perception of protocols, and why we can finally say that protocols in Swift are completely different from what they are in Objective-C.</p>

<p>Those changes originated from the discussion initiated by Joe Groff at the forum (back in April 2019 <a href="https://forums.swift.org/t/improving-the-ui-of-generics/22814">“Improving the UI of generics”</a>) and have been gradually released in 5.x Swift versions. <em>(The initial post was itself a result of previous discussions among the members of the Core team and can be traced back to 2016, the time in between Swift 2 and Swift 3 when <a href="https://github.com/apple/swift/blob/main/docs/GenericsManifesto.md">Generics Manifesto</a> was created)</em>.</p>

<h3 id="the-dualism">The dualism</h3>

<p>From the origins of Swift, <em>protocol</em> (as a language feature) was always in between two completely different worlds: compile-time constraints and runtime flexibility.</p>

<h4 id="runtime-polymorphism">Runtime polymorphism</h4>

<p>From Objective-C protocols inherited their dynamic essence. In this meaning “protocol” is what called “interface” in most of other languages. It’s a capability to define some contract (variables, functions) which then will be implemented by specific data types. So the compiler doesn’t know about a specific type, and exact implementation of the contract is being “attached” only in runtime using <em>dynamic method dispatch</em>. It’s more flexible, but less specific and impossible to optimise by the compiler.</p>

<p>Here is an example of runtime polymorphism:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protocol</span> <span class="kt">Animal</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="kt">Cat</span><span class="p">:</span> <span class="kt">Animal</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="kt">Dog</span><span class="p">:</span> <span class="kt">Animal</span> <span class="p">{}</span>

<span class="k">var</span> <span class="nv">someAnimal</span><span class="p">:</span> <span class="kt">Animal</span> <span class="o">=</span> <span class="kt">Cat</span><span class="p">()</span>
<span class="n">someAnimal</span> <span class="o">=</span> <span class="kt">Dog</span><span class="p">()</span> <span class="c1">// we can reassign a value of another type to this variable</span>
<span class="k">var</span> <span class="nv">animals</span><span class="p">:</span> <span class="p">[</span><span class="kt">Animal</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Cat</span><span class="p">(),</span> <span class="kt">Dog</span><span class="p">()]</span>
</code></pre></div></div>
<p><code class="language-plaintext highlighter-rouge">Animal</code> is a specific type both for the variable <code class="language-plaintext highlighter-rouge">someAnimal</code> and for the element of the <code class="language-plaintext highlighter-rouge">animals</code> collection. Compiler has no idea about the exact types regarding those vars. The exact type doesn’t matter here at all. So protocol behaves like a type itself. This type is called <strong>existential type</strong>. As you can see variable of existential type can hold values of different concrete types as far as they conform to the protocol. Collection of existentials can be heterogeneous, meaning it can also hold values of different concrete types.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="nf">pat</span><span class="p">(</span><span class="nv">animal</span><span class="p">:</span> <span class="kt">Animal</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// `animal` has a value of existential type `Animal`</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="compile-time-polymorphism">Compile-time polymorphism</h4>

<p>At the same time in Swift world protocol always played a big role in <em>generics</em>. Generics is a system that allows us to specify a set of requirements/constraints that can be later translated by a compiler into specific types. As the types are clear in compile time, the implementations are “connected” to the calls at that stage as well, so <em>static method dispatch</em> is being used. Hence the compiler can help you a lot here during development as well as optimising the code while compiling it.</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">func</span> <span class="n">pat</span><span class="o">&lt;</span><span class="kt">T</span><span class="p">:</span> <span class="kt">Animal</span><span class="o">&gt;</span><span class="p">(</span><span class="nv">animal</span><span class="p">:</span> <span class="kt">T</span><span class="p">)</span> <span class="p">{</span>
    <span class="c1">// `animal` is a value of some specific type, conforming to `Animal`</span>
<span class="p">}</span>

<span class="nf">pat</span><span class="p">(</span><span class="kt">Dog</span><span class="p">())</span>
<span class="nf">pat</span><span class="p">(</span><span class="kt">Cat</span><span class="p">())</span>
</code></pre></div></div>

<p><strong>Generic type</strong> <code class="language-plaintext highlighter-rouge">T</code> is being resolved into a specific type for each function call. So we know the exact type (<code class="language-plaintext highlighter-rouge">Cat</code> or <code class="language-plaintext highlighter-rouge">Dog</code> in this case) inside the <code class="language-plaintext highlighter-rouge">pat()</code> function. In this simple example it doesn’t bring us much value, but in a more complex construction it may have a big difference.</p>

<h4 id="alternative-terms">Alternative terms</h4>

<p>On the Swift forum you may also encounter such terms as <strong>type-level</strong> and <strong>value-level abstraction</strong>. They describe the same dualism from the perspective of the compiler.</p>

<p>Generic types (that represent compile-time polymorphism) provide abstraction on a type level so the compiler resolves the abstract types and then can already operate specific types, completely preserving their details (functions, properties). So we have <strong>type-level abstraction</strong> here.</p>

<p>In case of existentials and runtime polymorphism the exact types covered by protocols are not known for the compiler, so they are replaced by existential types (one protocol - one existential type). So the compiler have only ideas about the existential type and the values that will be assigned to it in runtime. Hence <strong>value-level abstraction</strong>.</p>

<p>In <a href="https://forums.swift.org/t/improving-the-ui-of-generics/22814#type-level-and-value-level-abstraction-1">this forum post</a> you can read more about it.</p>

<h3 id="the-paradox">The paradox</h3>

<p>This may not sound like an important thing, but there was a lot of confusion and frustration among the developers regarding the dualism of protocols, up until recently (before the discussed changes were released). Sometimes you were not allowed to use the same protocols in different use cases mixing runtime and compile-time capabilities. In some situation it could also cause some errors and unexpected behaviours (both in runtime and in compilation time). <em>(In our previous articles <a href="/protocol-faces/">“Several faces of protocols”</a> and <a href="/do-protocols-break-srp/">“Do protocols break Single Responsibility Principle?”</a>; we dived into the subject in more details)</em></p>

<p>There were always different opinions inside the Swift Core Team and most active part of the community regarding Protocols. Some people admitted that there was this feature dualism between the runtime and compile-time capabilities. For instance Dave Abrahams <a href="https://forums.swift.org/t/lifting-the-self-or-associated-type-constraint-on-existentials/18025/42">in one of the discussions on the forum</a> said:</p>
<blockquote>
  <p>I have always thought a big part of our problem is that protocols that are meant to be used for type erasure are fundamentally different from those meant to be used as constraints, yet we declare them the same way.</p>
</blockquote>

<p><em>(The author of this post also belonged to this camp, as you could guess from the previous articles. We even argued that it could have been better to have two separate language features instead of one).</em></p>

<p>But the majority considered all the capabilities of protocols as one big feature, that temporary had some gaps and contradictions.</p>

<p>Since the first versions, protocols in Swift played more important role as generic types other than existential ones. Runtime polymorphism is less safe, predictable and controllable (as it puts developer in charge, not a compiler), it erases type details, it implies dynamic memory allocation and reference counting, it’s slower and cannot be optimised by compiler. Compile-time capabilities in contrary were more interesting for the swift core team and the community as they are integrated into compiler and interact with other compile-time features.</p>

<p>Existential type is quite a simple language concept, so it wasn’t even widely discussed until recently (not much to talk about). So most public discussions regarding protocols are being held in the context of generics. <em>(There is an opinion that Swift as “Protocol-oriented language” means more “Generics-oriented language”… but it doesn’t sound as fancy)</em>.</p>

<p>Existential type is what protocols in Swift inherited from Objective-C as “default behaviour”. You didn’t need any additional syntax to define or return an existential type. In contrary the other features like generic types, when-condition, opaque types require some specific words or constructions. Eventually it started to look paradoxical that <em>major features</em> of protocols related to the generic types require more explicit syntax then <em>secondary feature</em> - existentials. The ideas how to tackle it had been discussed for quite some time before the changes even got to proposal stages.</p>

<p>But first things first…</p>

<h3 id="filling-in-the-gaps">Filling in the gaps</h3>

<p>To consider all the capabilities as one feature some visual gaps should have been fixed.</p>

<p>The biggest gap, as the Core team saw it, was the absence of the ability to return a generic type from a function. Existentials could be used both as parameters and result values. But on a compilation level you could only pass a generic type as a parameter, but were not able to describe the result value. That’s how <strong>opaque types</strong> appeared (not without SwiftUI playing a noticeable role).</p>

<p>When opaque types were introduced and then expanded to more use cases, the next challenge was to minimize the interference between generic and existential types. Meaning that one shoul be possible to use in case of the other.</p>

<p>The idea of so called <strong>generalized existentials</strong> was already mentioned in <a href="https://github.com/apple/swift/blob/main/docs/GenericsManifesto.md#generalized-existentials">GenericsManifesto</a>. The essence was to make it possible to use generic-type protocols (the ones with associated type or self-constrained) as existential. Another challenge was to turn such existential back to generic when you need it. Some smaller potential improvements were needed for specific use cases. Some of the feature limitations were artificial (kind of legacy), the other required significant changes, but most of the goals were achieved. A number of discussions were held on forum followed by proposals, and eventually several feature changes were released into the language (see References).</p>

<p>As a result, mixing existentials and generic types became seamless. You can create a generic type protocol and use it as existential and vice-versa. The amount of related compilation issues drastically decreased. Now understanding the difference between runtime and compile-time cases is not needed in most of the cases… as it just work.</p>

<h3 id="completing-the-paradigm-shift">Completing the paradigm shift</h3>

<p>But one “small” change stands out of the list of improvements: <strong>explicit existentials</strong> (introducing <code class="language-plaintext highlighter-rouge">any</code>). It didn’t add any new capabilities.</p>

<p>As time was passing by since the beginning, developers were more and more discouraged to use Existentials. Compile-time capabilities of protocols on the other hand were more and more extended and promoted. They became heavier in functionality and more valuable for the developers. Every other developer, when talking about protocols, kept enumerating the capabilities without even mentioning the original runtime polymorphism. It was just some small feature in the back of their minds, almost a nice side-effect of using protocols.</p>

<p>People in the community (first of all, the Core team) kept questioning the status quo: having existentials as default was considered less and less acceptable. But you cannot just swap the syntax for two different language features, so it was decided to deprecate the default behaviour and create a special syntax for existentials.</p>

<p>That’s why in case of existential declarations (see the code example before) we now should write:</p>

<div class="language-swift highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">var</span> <span class="nv">someAnimal</span><span class="p">:</span> <span class="n">any</span> <span class="kt">Animal</span> <span class="o">=</span> <span class="kt">Cat</span><span class="p">()</span>
<span class="k">var</span> <span class="nv">animals</span><span class="p">:</span> <span class="p">[</span><span class="n">any</span> <span class="kt">Animal</span><span class="p">]</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Cat</span><span class="p">(),</span> <span class="kt">Dog</span><span class="p">()]</span>
</code></pre></div></div>

<p>I’m saying “should” but now it’s a transitive period before the old syntax gets deprecated (expected in the next major language version). So we actually “have to” adopt this new way.</p>

<p>The change makes usage of existentials more explicit. Now it becomes a conscious choice rather than default option. It also eliminates some confusion when mixing existentials with compile-time constrains.</p>

<p>But the most important thing here, imho, is the shift of the protocol paradigm, from legacy Objective-C like runtime interface to a big part of a compile-time abstraction. The shift started when protocols were introduced in Swift, but only now it seem to be completed. From now on protocols will be perceived differently, now there are no doubts that protocols are first of all made for generic or opaque types, associated values, where-conditions and so on, and runtime polymorphism officially takes the second (third, fourth) place.</p>

<p>What can we see in future?</p>

<p>Who knows, maybe eventually the default syntax (defining a value, parameter or result with a protocol without any additional words) will be reassigned back, but this time to the generic/opaque types. That would look like a logical completion of the shift. Possibly in a couple of major releases we will see such syntactic simplification as a new feature in Swift.</p>

<p>The other possible continuation of this story could be compiler warnings that advise you not to use existential when your use case can be covered by a generic/opaque type (Isn’t it already there? Seems logical and easy to implement… but I haven’t heard about it yet).</p>

<p>Even though we might see some other enchancements in this area, as well as further development for the compile-time capabilities of protocols, the shift of the paradigm is completed. Bye-bye dynamic interfaces!</p>

<h2 id="references">References</h2>

<h4 id="main-changes-regarding-opaque-types">Main changes regarding opaque types:</h4>

<ul>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0244-opaque-result-types.md">[SE-0244] Opaque Result Types </a>  <br />
Introducing opaque return types. That’s the feature that was introduced mainly for SwiftUI back in Swift 5.1</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0328-structural-opaque-result-types.md">[SE-0328] Structural opaque result types</a>	
Extending the use cases for the opaque return type. Now it can be a part of another result structure (a tuple or a closure)</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0341-opaque-parameters.md">[SE-0341] Opaque Parameter Declarations</a>  <br />
An ability to use opaque types as parameters: more lightweight syntax for parameters with compile-time polymorphism.</li>
</ul>

<h4 id="additional-changes-regarding-to-opaque-types">Additional changes regarding to opaque types:</h4>

<ul>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0346-light-weight-same-type-syntax.md">[SE-0346] Lightweight same-type requirements for primary associated types</a> <br />
An ability to use more precise and complex compile-time parameters with more lightweight syntax.</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0360-opaque-result-types-with-availability.md">[SE-0360] Opaque result types with limited availability</a>  <br />
More future-proof abstraction of opaque types. Now it allows you to return new types as previously declared opaque types. Capability specifically added for making APIs with opaque result types less breakable.</li>
</ul>

<h4 id="main-changes-regarding-existential-types">Main changes regarding existential types:</h4>

<ul>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0309-unlock-existential-types-for-all-protocols.md">[SE-0309] Unlock existentials for all protocols</a><br />
Now generic type protocols can be used in some cases (still with limitations) as existentials.</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md">[SE-0335] Introduce existential any</a>  <br />
Making usage of existencial types more explicit.</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0352-implicit-open-existentials.md">[SE-0352] Implicitly Opened Existentials</a>  <br />
An ability to use existential types in some compile-time constrained cases (generics).</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0353-constrained-existential-types.md">[SE-0353] Constrained Existential Types</a> <br />
Making it possible to use compile-time constrained protocol as existensials and still keep those constraints (being able to utilize them after)</li>
  <li><a href="https://github.com/apple/swift-evolution/blob/main/proposals/0375-opening-existential-optional.md">[SE-0375] Opening existential arguments to optional parameters</a>  <br />
Allows an argument of (non-optional) existential type to be opened to be passed to an optional parameter.</li>
</ul>

<h4 id="key-discussions-at-the-forum">Key discussions at the forum:</h4>

<ul>
  <li><a href="https://forums.swift.org/t/improving-the-ui-of-generics/22814">Improving the UI of generics</a></li>
  <li><a href="https://forums.swift.org/t/lifting-the-self-or-associated-type-constraint-on-existentials/18025">Lifting the “Self or associated type” constraint on existentials</a></li>
  <li><a href="https://forums.swift.org/t/improving-the-ui-of-generics/22814">Improving the UI of generics</a></li>
  <li><a href="https://forums.swift.org/t/reverse-generics-and-opaque-result-types/21608">Reverse generics</a></li>
</ul>

<h4 id="other-relevant-reading">Other relevant reading:</h4>

<ul>
  <li><a href="https://medium.com/@PavloShadov/https-medium-com-pavloshadov-swift-protocols-magic-of-dynamic-static-methods-dispatches-dfe0e0c85509">Swift Protocols: Magic of Dynamic &amp; Static methods dispatches ✨</a></li>
  <li><a href="https://www.hackingwithswift.com/articles/247/whats-new-in-swift-5-6">What’s new in Swift 5.6</a></li>
  <li><a href="https://www.hackingwithswift.com/articles/249/whats-new-in-swift-5-7">What’s new in Swift 5.7</a></li>
  <li><a href="https://www.swiftbysundell.com/articles/referencing-generic-protocols-with-some-and-any-keywords/">Using the ‘some’ and ‘any’ keywords to reference generic protocols in Swift 5.7</a></li>
</ul>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="iOS" /><category term="swift" /><category term="protocols" /><category term="obj-c" /><summary type="html"><![CDATA[In this article we will be talking about the recent changes related to protocols: opaque, existential and generic types; some and any; runtime and compile-time polymorphism.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2023-01-10-protocol-paradigm-shift/butterfly-1920.jpg" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2023-01-10-protocol-paradigm-shift/butterfly-1920.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Modularity. What problem does it solve?</title><link href="http://dmtopolog.com/modularity-3-problems" rel="alternate" type="text/html" title="Modularity. What problem does it solve?" /><published>2022-10-03T00:00:00+00:00</published><updated>2022-10-03T00:00:00+00:00</updated><id>http://dmtopolog.com/modularity-3-problems</id><content type="html" xml:base="http://dmtopolog.com/modularity-3-problems"><![CDATA[<p>In this article we will discuss the problem that can be solved by modularity and how exactly modularity can make you project much better thing to deal with.</p>

<p>This post is part of the series on modularity:</p>
<ul>
  <li><a href="/modularity-1-boundaries">Modularity. Boundaries</a></li>
  <li><a href="/modularity-2-encapsulation">Modularity. Encapsulation</a></li>
  <li><a href="/modularity-3-problems"><strong>Modularity. What problem does it solve?</strong></a></li>
</ul>

<p> </p>

<h1 id="when-does-the-time-come">When does the time come?</h1>

<p>When is it right time to think about modularization in your project?</p>

<p>It doesn’t look like you should divide your project into the modules from the very creation. You definitely need to follow all the major architectural principles (separation of responsibilities, clear dependencies, architectural layers, DRY, KISS etc.) when building your logic, but you are likely not able to foresee how the project will look like in a year or even several months. Your business ideas, direction of development, target audience, your main features… all may change. At the beginning of its existence, your app can turn into something completely different within several months. So, it doesn’t make sense to start dividing and incapsulating high-level parts of business logic at this point. If you start to design your modules and interactions between them too early, you will be constantly wasting time on restructuring and changing.</p>

<p>The other extremum is when the situation has already gone so far that you need to stop (or at least significantly slow down) development of your features to modularize the project. At this point you might already have more than a dozen of developers, serious commitment to deliver features, ambitious goals… but the project is a mess, technical debt is enormous, bugs are all around and releasing new features gets harder and harder. If you found yourself here, then you should have already modularized your project some time ago. I’m not saying that now it’s already “too late”, no, but it costs you much more troubles to do it now than before.</p>

<p>As always, the truth is somewhere in between, and of course it’s not the same for different teams and different projects.</p>

<h3 id="making-the-terms-clear">Making the terms clear</h3>

<p>First let’s clarify what we mean when you say that the project is <strong>a monolith</strong>.</p>

<p>It might have different dependencies, some of them might be your own modules or subprojects. You might already have some separated modules for common data types, or design components, or network layer. You might have several different targets for extensions (watch, Siri, widgets, etc.). BUT more than 50% of your logic belongs to one “main” target (you can roughly count the number of lines of code together with the number of classes/functions). So, you have a monolith.</p>

<p>When we say that the app/project is <strong>modular</strong> we mean that it consists of a number of modules (separate targets inside one project or one main project and several dependent ones). All the code is spread across those separate logical parts and there are strict physical boundaries between them (read more about the boundaries and different types of projects in <a href="/modularity-1-boundaries">our previous article</a>).</p>

<h1 id="reasons-to-think-about-modularization">Reasons to think about modularization.</h1>

<p>Let’s try to figure out some concrete signs of the project that requires modularization. If your project has some of them it’s likely the time to modularize it.</p>

<p>The most popular reason for moving some code to a separate modules is situation when you need to share some logic between several apps. It might be network layer, common data types, functions or UI elements. We omit it here as it’s quite intuitive and doesn’t require much explanation. But there are more than that.</p>

<h3 id="poor-logical-separation">Poor logical separation</h3>

<p>At some point your project becomes so big and the flows are so complex that there is not a single developer who understands how all the features work. Plenty of features are not a compilation per se, but quite often those features are poorly separated architecturally. It’s really hard to measure such “architectural health” but if you start asking yourself the following questions you are likely to understand how good/bad the situation is.</p>
<ul>
  <li>Do you have clear boundaries between the screens, flows and features?</li>
  <li>Do you clearly understand the dependencies of the features?</li>
  <li>How easy it is to use your features/flows in a different context (in a different part of the app)?</li>
  <li>How do you handle shared functions between the features?</li>
  <li>Do you have any incapsulated services for networking, persistence, analytics, logging, error handling… or maybe you put all of these to the extensions (not the best approach)?</li>
  <li>Do you have a lot of data types that don’t really belong to anything?</li>
  <li>Do you use Dependency Injection?</li>
  <li>How many singletons do you have?</li>
  <li>How often do you prefer to use a singleton instead of passing a dependency explicitly?
etc.</li>
</ul>

<p>One of the bad indicators is situations when you try to grasp the context of an unfamiliar feature/service… but you just can’t. It’s not properly separated from the others; you cannot easily figure out all of the dependency. When tracking down the behavior in the debugger you keep on jumping from one place to another between separate classes and even parts of the app.</p>

<p>The more features you have, the more obvious your architectural imperfections are.</p>

<h3 id="a-lot-of-conflicts">A lot of conflicts</h3>

<p>Not only conflicts in pull requests matter, but generally how often one developer blocks another in their daily work. Starting from a team of 2 developers you need to somehow synchronize the work and coordinate the effort. With a badly architectured app and some decent number of features it can be painful for just 2 devs to work together. If the health of the code base is better, then even 3-5 developers can work on one monolithic project with a decent (though not the best) efficiency. But the more the team grows the more time on coordination you need to spend. Deliveries never grow proportionally to the size of team (twice more developers are never able to deliver twice more features), but if the amount of deliveries hardly increases when scaling the team that’s a bad sign.</p>

<p>The chances are that you do have some separation of logic and areas of responsibility for developers. Maybe you even have some feature teams dedicated to some specific app parts. But ask yourself how clear the boundaries between them are. How much code reside in the “gray areas” which don’t belong to anything? How often it’s not clear who supposed to work on this or that piece of logic? Or the other way around: there are some parts of the app that are constantly being changed by different developers (because many features/services depend on it).</p>

<p>At some point (sooner than later) constant conflicts and lack of clarity may cause serious frustration in the team (which, as we know, decreases satisfaction level and productivity and can even cost you some team members)</p>

<h3 id="compilation-speed-is-unbearable">Compilation speed is unbearable</h3>

<p>If you have a big monolithic target you need to recompile it entirely after every small change. As far as I understand the compiler still tries to make some smart chooses to decrease the compilation time (more with ObjC code then with Swift), but it doesn’t significantly change the situation. Hence when debugging the code, changing and rebuilding it all the time takes an eternity (can easily reach an hour or two cumulatively during the day). You frequently distract yourself, read articles, browse social media or do something else, so the efficiency drops even lower.</p>

<h3 id="re-running-all-the-tests">Re-running all the tests</h3>

<p>Quite likely if your project is not modularized your test coverage is not so high. Testability and modularization are not strictly connected, but there is a correlation. But let’s imagine that the architectural health is decent in your project, and even if it’s a monolith the code is testable and different parts are logically separated from each other. So, it is possible to have a good test coverage. Let’s keep on dreaming and consider you have it. In this case you need to run all the tests (locally or on CI) for every little change. And as the codebase (and test coverage) get bigger it becomes more and more frustrating (hundreds of UI tests take tens of minutes).</p>

<p>I heard about some smart solutions when people create some functionality clusters and per each change define what part of the tests should be rerun. But in a monolith, it’s just an overkill and waste of resources.</p>

<hr />

<p>The points above are quite subtle, there are no clear metrics or specific app qualities that you can monitor. It also usually hard to sell it to business to get time for the necessary refactoring. But if you proceed thinking further you can easily see how they affect the main properties of your product.</p>

<h3 id="decreasing-project-reliability">Decreasing project reliability</h3>

<p>It’s a combination of bad architectural separation and code base growth (not talking about general code quality). When you have “spaghetti code” where everything depends on everything it’s hard to change something without breaking something else. You might notice that the number of bugs, crashes and other side effects increases. That quickly influences your users, their satisfaction and opinion about your app (the rating, in-apps or sales inside the app logically go down).</p>

<h3 id="decreasing-efficiency">Decreasing efficiency</h3>

<p>You spend more time on the maintenance and fixes, more time to synchronize work of different developers, more time on compiling, debugging and testing the code, slow CI pipelines together with high probability of merge conflicts make every merge long and painful, it takes hours and even days to figure out how things work inside an unfamiliar piece of logic… Eventually the development of the new features stalls. If you delivered several features per week/month/sprint before, now this amount noticeably decreased. It’s hard to stick to the planning and meet the deadlines because issues pop here and there all the time.</p>

<p>Maybe you already hear such complaints from the business.</p>

<h1 id="how-does-modularization-help-you">How does modularization help you?</h1>

<p>I’m not saying that all the issues will be magically fixed if you modularize your project. Each of the issues above normally has several reasons and all cases are different. You definitely need to also think about using advanced architectural patterns and best practices, apply some code guidelines and QA-tools, check your work processes and the informational channels inside the team. But I’m sure that modularization will help you to improve the situation in all those aspects.</p>

<p>On a high-level, modular project is more structured than the monolith. Module is a separate unit, an extra layer of abstraction. When working on the host app (outside the module) we deal with modules instead of vague “parts” or “layers” that didn’t have clear responsibilities and boundaries. Specific data types on this level become just implementational details, so we can cut these details off. When working inside the module we don’t need to think about the context of the entire app, it doesn’t matter anymore while the module keeps its interface stable. So regardless of your context you decrease the cognitive load by reducing the context.</p>

<p>Modules are independent projects (even when having dependencies from each other) so the scope for the work processes also decreases. We are talking about several small project instead of one big monolith, hence we have independent, much faster development loops (develop, test, release). We have smaller number of features and much less conflicts: now instead of 10 devs working on the same monolith we have 5 teams of 2 devs working each on independent module.</p>

<p><em>(More details about context separation, and modules as independent projects you can read <a href="/modularity-2-encapsulation">here</a>)</em></p>

<p>We don’t have problems with long compilation and lengthy test runs anymore, as all of these is specific to the module. (You still need to debug and test the integration code, but the amount of work decreases several fold).</p>

<p>In big teams (when more than 4-6 devs work on one app) organizational structure normally changes together with the project. Modules work better when they have clear maintainers. So, the next logical step might be to establish some feature teams with clear areas of responsibility.</p>

<p>No-brainer that because all of this the overall productivity and satisfaction increases because generally it just gets easier to work. The prerequisite-consequence connection here is quite straightforward.</p>

<p>Regarding the reliability, reducing both the context and the connections between the parts of the app (and side effects as a result) changes the situation dramatically (in a good way of course =)). Code changes become easier to make and test, issues become easier to localize and debug.</p>

<p><em>(It was a high level overview of the benefits. In future posts I plan to talk more in details about each one of them: scalability, compilation, work processes etc. mentioning also the challenges that modularisation brings)</em></p>

<h2 id="bonus-improvements">Bonus improvements</h2>

<p>I already mentioned that there are a number of things that influence the maintainability and reliability of your code that are not directly related to modularization. Some of them are also got improved on the way together with modularization. Some changes are inevitable side effect of the refactoring needed for modularization. Some other improvements just look more logical in a new modular context. The others were just waiting in the backlog for ages and transition to the modular architecture is a good occasion to fix it on the way.</p>

<p>Some examples:</p>

<p>Usage of <strong>dependency injection</strong> and less <strong>singletons</strong> will come naturally together with modularization. When you don’t have implicit shared state between the modules you are forced to pass the dependencies explicitly. Inside the modules you can still avoid it and use the dark side of the force (singletons and internal instantiation of dependencies), but you are likely to start questioning this approach as it is not a default approach. If you know all about DI and singletons and just were waiting for the occasion to refactor some legacy shared state (which usually requires quite some changes all around the app), migration to the modules is the time to do it.</p>

<p>You might also rethink some responsibilities in your code (as now you need to draw bold lines between them), on the way you might refactor a couple of <strong>god classes</strong> the ones that have couple of thousands of lines of code and a bunch of different responsibilities.</p>

<p>Modularity itself and advanced architectural techniques will increase <strong>testability</strong> of your code.</p>

<p>When designing your modules, you normally would discuss inside the team some general architectural patterns and approaches that your future modules supposed to share. If you go further, you may even write down some guidelines on how the future modules should be created, structured and maintained. That will bring more <strong>architectural and/or code style consistency</strong> to your code base that increases the productivity even more in future.</p>

<p>You can refactor some small pieces of the <strong>legacy ObjC code</strong> that just don’t fit your new modular frame of the project.</p>

<p>Of course, you should understand that making several migrations on the same time is not a right thing to do. As well as fixing too much of technical debt simultaneously. So, if you have some small things here and there, go ahead and fix them on the way along with modularization. But if the side fixing of something requires too much additional effort, please put it aside and pick it up later when modularization is over. I know how difficult it can be and how big is the temptation to cure all the diseases at once. I’m sure you will have enough work to do with modularization only, so don’t unnecessary increase the scope even more. All your dirty legacy will be cut into pieces and nicely split into modules together with the rest of your code, so it will be anyway easier to handle it after the modularization.</p>

<p> </p>

<hr />
<p>I hope you liked this piece of reading. If you have any questions, suggestions or corrections you can reach me out <a href="https://twitter.com/dmtopolog">on Twitter</a></p>]]></content><author><name>Dmitrii Ivanov</name></author><category term="Tech Blog" /><category term="modules" /><category term="iOS" /><category term="architecture" /><category term="no code" /><category term="platform-agnostic" /><summary type="html"><![CDATA[In this article we will discuss the problem that can be solved by modularity and how exactly modularity can make you project much better thing to deal with.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="http://dmtopolog.com/images-posts/2022-10-03-modularity-3-problems/nails-1920x620.jpg" /><media:content medium="image" url="http://dmtopolog.com/images-posts/2022-10-03-modularity-3-problems/nails-1920x620.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>