📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

πŸ“š Interview question

What is evolutionary architecture, and how do you design a .NET system so its architecture can change safely over time?

Evolutionary architecture: fitness functions, incremental change, last responsible moment, reversible decisions, modular monolith to services, ADRs, and architecture tests in CI.

Staff / Architect Architecture Expert design-judgment evolutionary-architecture fitness-functions modular-monolith

🎯 Key points

  • Architecture that supports guided, incremental change across multiple dimensions
  • Fitness functions are automated checks that protect key architectural characteristics
  • Defer irreversible decisions to the last responsible moment; prefer reversible ones
  • Modular boundaries (modular monolith) allow later extraction into services
  • ADRs record decisions and their context so they can be revisited
  • Deployment pipelines, feature flags and observability make change safe

⏱ 30-second answer

Evolutionary architecture (Ford, Parsons and Kua) is architecture designed to support guided, incremental change, rather than a fixed upfront design. The key tool is fitness functions: automated checks that protect the characteristics you care about, such as dependency rules (architecture tests), latency budgets, or startup time, run in CI or production. Around that, I keep boundaries modular (often a modular monolith first), make decisions reversible where possible and defer irreversible ones to the last responsible moment, record decisions in ADRs, and rely on continuous delivery, feature flags and observability so each change is small and safe.

πŸ—£ Full answer (senior)

How I would answer this in an interview

"Evolutionary architecture, from Neal Ford, Rebecca Parsons and Patrick Kua, is architecture that supports guided, incremental change across several dimensions, instead of a fixed design produced upfront. My goal isn't to pick the right architecture on day one. It's to build one that can become right as we learn. The central tool is fitness functions: automated, objective checks that protect the characteristics we care about. In .NET those are architecture tests with NetArchTest or ArchUnitNET for dependency and module rules, performance budgets with BenchmarkDotNet or k6, SLO alerts on OpenTelemetry metrics, vulnerability scans, OpenAPI breaking-change diffs, and startup or image-size budgets, all running in CI or continuously in production. Around that I keep boundaries modular with owned data (usually a modular monolith first, so modules can be extracted into services later when there's a real scaling, cadence or ownership reason). I prefer reversible decisions and defer irreversible ones to the last responsible moment. I record decisions in ADRs so they can be revisited when the context changes. And I rely on continuous delivery, feature flags and observability so every change is small and safe. Evolving architecture is also a people exercise: Conway's law means team structure and stakeholder alignment have to move with it."


1. Principles

Principle In practice
Incremental change Small, frequent deploys; strangler fig / branch by abstraction for big shifts
Guided by fitness functions Automated checks for the characteristics that matter
Appropriate coupling Modules with explicit public APIs and owned data
Last responsible moment Decide when you know enough, not earlier, but not so late that options vanish
Reversibility Prefer choices that are cheap to undo; isolate expensive ones behind boundaries
Experimentation Feature flags, canary releases, A/B tests to learn safely

2. Fitness functions in .NET

Characteristic Fitness function Where it runs
Layering / module boundaries NetArchTest or ArchUnitNET tests CI
Performance BenchmarkDotNet thresholds; k6 load tests against SLOs CI / nightly
Latency, availability, error rate SLO alerts from OpenTelemetry metrics (Azure Monitor, Prometheus/Grafana) Production (continuous)
Security dotnet list package --vulnerable, container scans; build fails on critical CI
API compatibility OpenAPI diff fails on breaking changes; public API analysers for packages CI
Startup time / image size Budgets checked in the pipeline (relevant for scale-to-zero, AOT) CI
Resilience Chaos experiments (e.g. Azure Chaos Studio) on dependencies Scheduled
Cost Budget alerts per environment/service Continuous

3. A typical evolution path

Stage What it looks like
1. Modular monolith One deployable; modules with internal implementation, public contracts, own EF Core DbContext and schema
2. Enforced boundaries Architecture tests in CI; in-process events between modules
3. Independent needs appear A module needs separate scaling, release cadence or team ownership
4. Extract Move the module behind its existing contract into a service; data already separate
5. Verify Fitness functions (latency, error rate) confirm no regressions
6. Record ADR for each step and its criteria

4. Decisions by reversibility

Decision Reversibility Approach
Internal module structure High Decide quickly, refactor freely
Message broker / cloud queue Medium Behind a port; keep messages broker-agnostic
Public API contracts, event schemas Low Design carefully; version; additive changes
Data ownership across modules Low Owned per module from the start
Managed cloud services (e.g. vendor-specific databases) Low–medium Weigh lock-in, cost and exit cost explicitly; abstract where worthwhile

Production example

We built a [B2B ordering] platform as a modular monolith with [five] modules, each with its own EF Core DbContext and schema, communicating through public contracts and in-process events. NetArchTest rules in CI blocked any reference to another module's internals, and production SLOs on checkout latency acted as continuous fitness functions. About [18 months] later, catalogue search needed independent scaling for a heavy search workload. Because the boundary and data were already separate, extracting the catalogue module into its own service took [about three weeks]. The latency fitness function confirmed checkout didn't regress, and an ADR recorded the extraction criteria so the next decision would follow the same rules.

πŸ” What the interviewer is checking

Checks strategic architecture thinking at staff level.

Green flags

  • Defines fitness functions with concrete .NET tools.
  • Discusses reversibility and last responsible moment.
  • Uses modular monolith as an evolution path.
  • Mentions ADRs, Conway's law and delivery pipelines.

Red flags

  • Architecture as a one-time upfront deliverable.
  • No way to enforce or measure architectural qualities.

πŸ“š Learn in depth

1. Origin

Building Evolutionary Architectures (Neal Ford, Rebecca Parsons, Patrick Kua; 2nd edition with Pramod Sadalage) defines it as architecture that "supports guided, incremental change across multiple dimensions". It borrows fitness functions from evolutionary computing: an objective measure of how close a solution is to its goals.

2. Kinds of fitness functions

Kind Example
Atomic One characteristic: "Domain doesn't reference EF Core"
Holistic Several together: "p95 < 300 ms under 500 RPS with auth and logging enabled"
Triggered Run on events: CI build, nightly job
Continuous Always on: production SLO monitoring
Static Fixed threshold (image < 200 MB)
Dynamic Threshold depends on context (latency budget scales with payload size)

Start with a few that protect what matters most; too many noisy checks get ignored.

3. Modular monolith mechanics in .NET

src/
  Modules/
    Ordering/
      Ordering.Contracts      (public: commands, events, DTOs)
      Ordering                (internal implementation, OrderingDbContext β†’ schema 'ordering')
    Catalogue/
      Catalogue.Contracts
      Catalogue               (CatalogueDbContext β†’ schema 'catalogue')
  Host/                       (composition root; references modules)
tests/
  ArchitectureTests/          (NetArchTest rules)
  • Modules reference only other modules' *.Contracts.
  • Implementation types are internal; InternalsVisibleTo only for the module's tests.
  • Each module owns its schema; no cross-module joins.
  • Cross-module reactions use in-process events (later replaceable by a broker with an outbox).

4. Extraction checklist

Ready when Check
No inbound references to internals Architecture tests pass
Data owned and separable Own schema; no cross-module joins or FKs
Communication via contracts/events No shared in-memory state
Clear reason Scaling, release cadence, team ownership, compliance isolation
Operational readiness Observability, deployment pipeline, on-call ownership

5. Last responsible moment and reversibility

Deciding too early locks in choices with little information; deciding too late lets options disappear (e.g. data model already shared). The trick is to make the expensive decisions reversible where possible (ports in front of vendors, owned data, versioned contracts) so fewer decisions need to be made early at all.

6. Conway's law and stakeholders

  • Systems mirror the communication structure of the organisation that builds them. If two teams share one module, expect coupling; if one team owns five services, expect them to merge in practice.
  • The inverse Conway manoeuvre shapes teams to match the desired architecture.
  • Architecture changes need product and leadership alignment: explain the trade-offs (cost, delivery speed, risk) and decide together; ADRs make those decisions visible.

7. ADRs

# ADR-0012: Extract Catalogue into a separate service
Status: Accepted (2026-05-10)
Context: Search load grew 8x; catalogue scaling forced scaling the whole monolith.
Decision: Extract Catalogue behind Catalogue.Contracts; keep events via outbox.
Consequences: +1 deployable, +network hop for product lookups; independent scaling; team X owns it.
Revisit if: search load drops or cross-module latency exceeds the SLO.

8. Try it yourself

  1. Write an architecture test forbidding your domain project from referencing EF Core; add it to CI.
  2. Add a rule that modules may only reference other modules' *.Contracts assemblies.
  3. Define one SLO-based fitness function (e.g. p95 latency) and an alert for it.
  4. Add an OpenAPI diff step to the pipeline that fails on breaking changes.
  5. Write an ADR for your current module boundaries, including "revisit if" criteria.

πŸ’» Code

// Fitness functions as xUnit tests with NetArchTest.Rules.
using NetArchTest.Rules;
using Xunit;

public class ArchitectureFitnessTests
{
    private static readonly System.Reflection.Assembly OrderingAssembly = typeof(Ordering.PlaceOrder).Assembly;

    [Fact]
    public void Ordering_domain_does_not_depend_on_infrastructure()
    {
        var result = Types.InAssembly(OrderingAssembly)
            .That().ResideInNamespace("Ordering.Domain")
            .ShouldNot().HaveDependencyOnAny("Microsoft.EntityFrameworkCore", "Ordering.Infrastructure")
            .GetResult();

        Assert.True(result.IsSuccessful, string.Join(", ", result.FailingTypeNames ?? []));
    }

    [Fact]
    public void Ordering_only_uses_other_modules_through_their_contracts()
    {
        var result = Types.InAssembly(OrderingAssembly)
            .ShouldNot().HaveDependencyOnAny("Catalogue.Internal", "Billing.Internal")
            .GetResult();

        Assert.True(result.IsSuccessful, string.Join(", ", result.FailingTypeNames ?? []));
    }

    [Fact]
    public void Module_implementation_types_are_not_public()
    {
        var result = Types.InAssembly(OrderingAssembly)
            .That().ResideInNamespace("Ordering.Internal")
            .ShouldNot().BePublic()
            .GetResult();

        Assert.True(result.IsSuccessful, string.Join(", ", result.FailingTypeNames ?? []));
    }
}

namespace Ordering
{
    public sealed class PlaceOrder { }
}

Requires xunit and NetArchTest.Rules. In a real solution the test project references the module assemblies; the PlaceOrder stub only gives typeof an anchor type.

⚠ Common mistakes

1. Designing the final architecture upfront

What happens: a detailed target architecture before real usage. Why it is wrong: requirements and scale change; the design fits a future that doesn't arrive. Do instead: modular boundaries, fitness functions and incremental change.

2. Starting with microservices "to be evolvable"

What happens: many services on day one. Why it is wrong: distribution makes boundaries expensive to move while they're still uncertain. Do instead: modular monolith; extract when there's a concrete reason.

3. Architecture rules only in a wiki

What happens: layering rules documented but not enforced. Why it is wrong: they erode under deadline pressure. Do instead: encode them as fitness functions in CI.

4. Shared database across modules that may become services

What happens: modules join each other's tables. Why it is wrong: blocks later extraction and couples every schema change. (A single shared database can be fine for a simple monolith that will never split, but it's the main obstacle to evolving one.) Do instead: module-owned schemas and DbContexts, communicating via contracts/events.

5. Making irreversible decisions early without need

What happens: committing to a vendor-specific service or data model before you understand needs. Why it is wrong: locks in choices with little information. Do instead: defer to the last responsible moment; prefer reversible options.

6. Too many noisy fitness functions

What happens: dozens of flaky checks. Why it is wrong: teams ignore or disable them. Do instead: a few reliable checks for the characteristics that matter most.

7. No production fitness functions

What happens: only CI checks. Why it is wrong: latency, error and cost regressions appear only under real load. Do instead: SLO alerts and dashboards as continuous fitness functions.

8. Ignoring Conway's law

What happens: architecture changed without changing team ownership. Why it is wrong: the system drifts back to match communication structures. Do instead: evolve team boundaries alongside module/service boundaries.

9. Decisions without recorded context

What happens: "why is it like this?" with no answer. Why it is wrong: teams either fear changing it or change it blindly. Do instead: ADRs with context, consequences and "revisit if" criteria.

10. Evolving without stakeholder alignment

What happens: engineers extract services or re-platform quietly. Why it is wrong: surprises in cost, delivery timelines and risk erode trust. Do instead: explain trade-offs in business terms and decide together.

πŸ› Senior insight: trade-offs

At staff level the goal is not the right architecture but an architecture that can become right as we learn. I invest in boundaries, automated fitness functions and delivery speed, because those make every future change cheaper.

Investment Payoff
Module boundaries with owned data Cheap extraction later
Architecture tests in CI Rules survive deadlines
SLO-based production fitness functions Detect regressions from change
ADRs Decisions can be revisited with context
Continuous delivery and flags Small, reversible changes

πŸ”— Follow-ups the interviewer may ask

  1. When: Candidate gives modules owned data Why: Probes coupling at system level
    How to answer

    Coupling is the degree to which one module depends on another: how much it must know about it, and how likely it is to change when the other changes. Some coupling is unavoidable; the goal is coupling that is explicit, narrow and stable. In code the harmful kinds are reaching into another type's internals, shared mutable global state and passing control flags. In distributed .NET systems the expensive kinds are temporal coupling (synchronous call chains), shared databases and lock-step deployments. I reduce them with narrow interfaces, published contracts, asynchronous messaging where it fits, and each service owning its data.

  2. When: Candidate extracts a module incrementally Why: Checks refactoring strategy
    How to answer

    I treat a large refactoring as a series of small, behaviour-preserving steps merged to main continuously, not a big-bang branch. First I pin current behaviour with characterisation tests. Then I pick a technique: strangler fig to route traffic piece by piece to a new implementation, branch by abstraction to put an interface in front of the old code and swap implementations behind it, or parallel change (expand, migrate, contract) for APIs and database schemas. Feature flags and shadow comparisons let old and new paths run side by side. I prioritise hotspots (code that changes often and hurts), keep refactoring commits separate from feature commits, and define an end state so it does not drag on forever.

  3. When: Candidate enforces dependency rules with tests Why: Links to dependency inversion
    How to answer

    DIP says high-level policy should not depend on low-level details; both should depend on abstractions, and the abstraction belongs to the high-level side. For example, the domain or application layer defines IPaymentGateway, and the infrastructure project implements it with Stripe; the arrow of source dependency points inwards. Dependency injection is a technique for supplying those implementations (constructor injection), and the .NET DI container is a tool that automates it. You can use a container without inverting anything (if interfaces live next to their implementations in infrastructure), and you can invert dependencies without a container.

πŸ›  Real-world problems: how you handle them

Catalogue module extracted in three weeks
Situation
A [B2B ordering] platform started as a modular monolith with [five] modules.
Signals
After [18] months, catalogue search load needed independent scaling.
Investigation
Fitness functions confirmed the catalogue module had no inbound dependencies on internals and its own schema.
Resolution
Extracted catalogue into a service behind its existing public contract; latency fitness function verified checkout was unaffected.
Prevention
Architecture tests and per-module schemas remain mandatory; ADR recorded the extraction criteria.

How to tell it (STAR)

We built the platform as a modular monolith with each module owning its data, and architecture tests in CI kept the boundaries honest. When catalogue search needed to scale on its own [18] months later, extracting it took [three] weeks because the boundary was already real. Our latency checks showed checkout didn't regress.

↩ Usually asked after

More in Senior-Level Judgment

Last updated 31 Oct 2026.

🌐 Sangam's Webring

βͺ Previous 🏠 Main Next ⏩
πŸ’¬ Chat with me