Skip to content

Design Patterns in .NET

Practical usage, trade-offs, and real-world design in production systems

Design patterns are one of those topics that are often taught in a way that makes them sound more formal than they really are.

In real production systems, a pattern is usually just this:

a repeatable way to solve a recurring design problem.

That is all.

Good engineers do not start from, “Which GoF pattern can I apply here?” They start from, “What problem keeps happening in this code, and what structure would make it safer, clearer, or easier to change?”

That is the mindset that matters in a technical leadership interview.


Part 1 — Big picture

Why design patterns exist

Patterns exist because large systems keep running into the same categories of problems:

  • how to swap one behavior for another
  • how to isolate external dependencies
  • how to coordinate many moving parts
  • how to represent changing state safely
  • how to extend behavior without rewriting core logic
  • how to avoid spreading conditional logic everywhere

When a system is small, you can often solve these problems with direct code. When a system becomes large, long-lived, and touched by many engineers, the same problems return again and again. Patterns become useful because they give you a shared design vocabulary and a proven shape for handling those problems.

In a production .NET system, patterns are less about elegance and more about survival.

For example:

  • In machine communication, you need a way to isolate vendor SDK quirks from your application logic.
  • In workflow orchestration, you need a way to model states and transitions without giant if/else blocks.
  • In WPF UI composition, you need a way to decouple UI actions from business logic.
  • In real-time event/result processing, you need a way for components to react to data without hard-wiring everything together.

Patterns help because they reduce chaos.

Patterns are reusable design ideas, not rigid rules

A pattern is not a law. It is not something you “must” implement exactly as described in a book.

In real .NET code, patterns are often softened, mixed, or adapted:

  • a Strategy may just be an interface with a few implementations registered in DI
  • a Factory may just be a small composition service
  • an Observer may be events, IObservable<T>, Channels, or a message bus
  • a Decorator may be a pipeline behavior or wrapper service
  • a State pattern may be a formal state object model, or it may just be an explicit state machine table

The value is not the name. The value is the problem-solution fit.

Why memorizing names matters less than understanding the problem

Interviewers sometimes ask about patterns, but strong senior answers are rarely about definitions.

Weak answer:

“Strategy is a behavioral pattern that allows selecting an algorithm at runtime.”

Strong answer:

“I use Strategy when the business or hardware behavior changes by mode, device type, or policy, and I don’t want that variation spread across conditionals in multiple places.”

That answer shows design judgment.

What matters is whether you understand:

  • what pressure in the system caused the pattern to appear
  • what trade-off the pattern introduces
  • what simpler alternative exists
  • when the pattern stops paying for itself

Why patterns are useful in large systems but dangerous when forced

Patterns help large systems because large systems have:

  • more variation
  • more dependencies
  • more engineers changing code
  • longer lifetimes
  • more production failures
  • more need for testability and extension

But patterns become dangerous when forced too early or too mechanically.

Common failure mode:

A team learns “clean architecture + patterns,” then creates:

  • interface for everything
  • factories for types that never vary
  • repositories for every table
  • mediator for trivial in-process calls
  • state objects for a workflow with only two stable branches

Now the code has more files, more indirection, more DI registrations, and more debugging pain, but not more clarity.

Patterns should reduce complexity at the system level, not increase it at the local level.


Part 2 — Most important patterns in real .NET systems

I will focus on the patterns that actually matter in modern production .NET systems.


1. Strategy

The real problem it solves

You have a stable workflow, but one part of the behavior varies.

Examples:

  • different inspection algorithms
  • different classification logic by product type
  • different retry policy by device type
  • different export format generation
  • different rules for defect scoring

Without Strategy, variation often turns into:

  • giant switch blocks
  • conditionals spread across services
  • duplicated workflow logic with only one step different

Strategy keeps the workflow stable and isolates the variable behavior.

How it appears in .NET systems

Usually as:

  • an interface
  • multiple implementations
  • selected by DI, config, machine type, or runtime context

For example:

csharp
public interface IInspectionStrategy
{
    InspectionResult Inspect(InspectionContext context);
}

public sealed class BrightFieldInspectionStrategy : IInspectionStrategy
{
    public InspectionResult Inspect(InspectionContext context)
    {
        // Bright-field specific inspection logic
        return new InspectionResult();
    }
}

public sealed class DarkFieldInspectionStrategy : IInspectionStrategy
{
    public InspectionResult Inspect(InspectionContext context)
    {
        // Dark-field specific inspection logic
        return new InspectionResult();
    }
}

Selection may happen through a resolver:

csharp
public interface IInspectionStrategyResolver
{
    IInspectionStrategy Resolve(InspectionMode mode);
}

public sealed class InspectionStrategyResolver : IInspectionStrategyResolver
{
    private readonly IServiceProvider _serviceProvider;

    public InspectionStrategyResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IInspectionStrategy Resolve(InspectionMode mode) =>
        mode switch
        {
            InspectionMode.BrightField => _serviceProvider.GetRequiredService<BrightFieldInspectionStrategy>(),
            InspectionMode.DarkField => _serviceProvider.GetRequiredService<DarkFieldInspectionStrategy>(),
            _ => throw new NotSupportedException($"Unsupported mode: {mode}")
        };
}

Why it is useful

  • keeps varying logic localized
  • reduces branching in higher-level workflow code
  • makes extension safer
  • improves testability
  • avoids contaminating orchestration code with mode-specific details

When it is overkill

If there are only two simple cases and they are unlikely to grow, a small conditional may be clearer than a full Strategy structure.

Bad use of Strategy:

  • creating five classes for logic that is only three lines long
  • using Strategy when there is no real variation pressure
  • splitting code so much that understanding the full workflow becomes harder

The pattern is justified when variation is likely to grow or when change frequency differs across behaviors.


2. Adapter

The real problem it solves

An external dependency does not fit your application model.

This is extremely common in industrial and hardware-integrated systems.

Examples:

  • vendor SDK uses blocking calls and awkward object models
  • camera API exposes callbacks you want to turn into async tasks
  • machine controller exposes raw numeric error codes instead of meaningful domain failures
  • a legacy COM API returns device-specific structures that pollute your app

Adapter isolates that mismatch.

How it appears in .NET systems

Usually as a wrapper around:

  • vendor SDKs
  • REST/gRPC clients
  • serial or TCP device APIs
  • legacy libraries
  • third-party services

Example:

csharp
public interface IMachineController
{
    Task ConnectAsync(CancellationToken cancellationToken);
    Task<HomeResult> HomeAsync(CancellationToken cancellationToken);
    Task<MachineStatus> GetStatusAsync(CancellationToken cancellationToken);
}

public sealed class VendorMachineControllerAdapter : IMachineController
{
    private readonly VendorSdkClient _sdkClient;

    public VendorMachineControllerAdapter(VendorSdkClient sdkClient)
    {
        _sdkClient = sdkClient;
    }

    public Task ConnectAsync(CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            _sdkClient.Initialize();
            _sdkClient.OpenConnection();
        }, cancellationToken);
    }

    public Task<HomeResult> HomeAsync(CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            int code = _sdkClient.RunHomeSequence();
            return code == 0
                ? HomeResult.Success()
                : HomeResult.Failure($"Vendor home failed with code {code}");
        }, cancellationToken);
    }

    public Task<MachineStatus> GetStatusAsync(CancellationToken cancellationToken)
    {
        return Task.Run(() =>
        {
            var raw = _sdkClient.ReadStatus();
            return new MachineStatus(
                isReady: raw.ReadyFlag,
                isBusy: raw.MotionState == 2,
                alarmCode: raw.AlarmCode);
        }, cancellationToken);
    }
}

Why it is useful

  • prevents vendor types from leaking everywhere
  • allows replacement or simulation in tests
  • gives you a domain-friendly API
  • centralizes translation of errors, threading, timeouts, and data conversion
  • protects the rest of the app from unstable external interfaces

When it is overkill

If the external library is tiny and used in exactly one place, a full abstraction may not help.

But in industrial systems, Adapter is usually one of the highest-value patterns because vendor integration is one of the biggest sources of long-term pain.


3. Factory / Abstract Factory

The real problem it solves

Object creation has logic, variation, or dependency on runtime context.

Examples:

  • different machine models need different service sets
  • simulation mode vs real hardware mode
  • camera pipeline creation differs by line configuration
  • recipe loads determine which processors are needed

Simple constructor calls are fine until creation itself becomes a design concern.

How it appears in .NET systems

In practice, often not as textbook “Abstract Factory” but as:

  • a composition service
  • named registrations
  • a builder/factory abstraction
  • a machine-specific component provider

Example:

csharp
public interface IMachineModuleFactory
{
    IMachineModule Create(MachineConfiguration configuration);
}

public sealed class MachineModuleFactory : IMachineModuleFactory
{
    private readonly IServiceProvider _serviceProvider;

    public MachineModuleFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public IMachineModule Create(MachineConfiguration configuration)
    {
        return configuration.MachineType switch
        {
            MachineType.Real => _serviceProvider.GetRequiredService<RealMachineModule>(),
            MachineType.Simulator => _serviceProvider.GetRequiredService<SimulatedMachineModule>(),
            _ => throw new NotSupportedException($"Machine type {configuration.MachineType} is not supported.")
        };
    }
}

Why it is useful

  • keeps creation logic out of business workflows
  • centralizes variant selection
  • reduces scattered conditionals
  • helps avoid direct new of infrastructure-heavy types in application code

When it is overkill

If construction is trivial and stable, a factory adds ceremony.

A common mistake is creating factories just to hide new. That is not enough reason. A factory is useful when creation itself has meaningful variation or orchestration.


4. Observer / Pub-Sub

The real problem it solves

One component produces events or data, and many other components may react.

Examples:

  • machine status updates
  • image captured
  • inspection result available
  • alarm raised
  • throughput metric updated
  • UI panel needing live updates

Without Observer or Pub-Sub, producers start knowing too much about consumers.

How it appears in .NET systems

Several ways:

  • .NET events
  • IObservable<T>
  • Channels
  • message brokers
  • in-process event bus
  • mediator notifications

Example with a result publisher:

csharp
public interface IInspectionResultPublisher
{
    void Publish(InspectionResult result);
}

public sealed class InspectionResultStream : IInspectionResultPublisher
{
    private readonly Channel<InspectionResult> _channel = Channel.CreateUnbounded<InspectionResult>();

    public ChannelReader<InspectionResult> Reader => _channel.Reader;

    public void Publish(InspectionResult result)
    {
        if (!_channel.Writer.TryWrite(result))
            throw new InvalidOperationException("Failed to publish inspection result.");
    }
}

Consumers:

csharp
public sealed class ResultPersistenceWorker
{
    private readonly ChannelReader<InspectionResult> _reader;
    private readonly IResultRepository _repository;

    public ResultPersistenceWorker(InspectionResultStream stream, IResultRepository repository)
    {
        _reader = stream.Reader;
        _repository = repository;
    }

    public async Task RunAsync(CancellationToken cancellationToken)
    {
        await foreach (var result in _reader.ReadAllAsync(cancellationToken))
        {
            await _repository.SaveAsync(result, cancellationToken);
        }
    }
}

Why it is useful

  • decouples producer from consumers
  • supports live streaming scenarios
  • enables adding new consumers without rewriting producer code
  • matches real-time processing naturally

When it is overkill

  • when there is only one consumer and likely to remain so
  • when event flow becomes invisible and debugging gets hard
  • when “pub-sub” is used to avoid clear ownership and control flow

Pub-Sub helps, but uncontrolled event-driven design can become a system where nobody knows who reacts to what.


5. State

The real problem it solves

Behavior changes depending on the current state, and invalid transitions matter.

This is extremely important in machine systems.

Examples:

  • Disconnected → Connecting → Ready → Running → Paused → Faulted
  • Idle → Loading Recipe → Inspecting → Finalizing → Completed
  • Alarm states and operator acknowledgment flows

Without an explicit state model, these systems often become:

  • booleans everywhere
  • flags that contradict each other
  • fragile transition logic
  • invalid operations slipping through

How it appears in .NET systems

Sometimes as formal state objects, but very often as an explicit state machine with transition rules.

Simple example:

csharp
public enum MachineState
{
    Disconnected,
    Ready,
    Running,
    Faulted
}

public sealed class MachineStateController
{
    public MachineState CurrentState { get; private set; } = MachineState.Disconnected;

    public void Connect()
    {
        if (CurrentState != MachineState.Disconnected)
            throw new InvalidOperationException("Machine can only connect from Disconnected state.");

        CurrentState = MachineState.Ready;
    }

    public void StartRun()
    {
        if (CurrentState != MachineState.Ready)
            throw new InvalidOperationException("Machine can only start from Ready state.");

        CurrentState = MachineState.Running;
    }

    public void Fault()
    {
        CurrentState = MachineState.Faulted;
    }
}

More advanced version uses per-state classes, but often a clear transition model is enough.

Why it is useful

  • makes valid transitions explicit
  • prevents impossible combinations
  • reduces hidden workflow bugs
  • improves recovery logic
  • helps UI and operations align with machine reality

When it is overkill

A full State object hierarchy can be too much if:

  • workflow is simple
  • transitions are few
  • state-specific behavior is limited

Sometimes an explicit enum + transition rules is more readable than many tiny state classes.


6. Command

The real problem it solves

An action should be represented as an object or encapsulated unit, separate from UI triggers and sometimes from execution timing.

In WPF, this pattern is everywhere.

Examples:

  • Start inspection
  • Stop machine
  • Acknowledge alarm
  • Export results
  • Retry connection

How it appears in .NET systems

Most visibly through ICommand in WPF.

csharp
public sealed class StartInspectionCommand : ICommand
{
    private readonly IInspectionWorkflow _workflow;

    public StartInspectionCommand(IInspectionWorkflow workflow)
    {
        _workflow = workflow;
    }

    public bool CanExecute(object? parameter) => _workflow.CanStart;

    public async void Execute(object? parameter)
    {
        await _workflow.StartAsync(CancellationToken.None);
    }

    public event EventHandler? CanExecuteChanged;

    public void RaiseCanExecuteChanged() =>
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}

In modern code, teams often use RelayCommand/AsyncRelayCommand from MVVM toolkits rather than hand-writing this every time.

Why it is useful

  • clean UI binding
  • separates user intent from execution logic
  • supports enable/disable semantics
  • can centralize authorization, validation, and state gating

When it is overkill

Outside UI or queued-operation scenarios, turning every method into a Command object can be excessive.

Also, WPF commands can become thin wrappers that add little value if the real logic is still tangled in the ViewModel.


7. Decorator

The real problem it solves

You want to add behavior around an existing service without modifying its core implementation.

Examples:

  • logging
  • retries
  • timeout enforcement
  • metrics
  • caching
  • authorization checks
  • tracing

Decorator is one of the most practical patterns in modern .NET.

How it appears in .NET systems

Either manually or via DI pipeline patterns.

Example:

csharp
public interface IMotionService
{
    Task MoveToAsync(Position position, CancellationToken cancellationToken);
}

public sealed class MotionService : IMotionService
{
    private readonly IMachineController _controller;

    public MotionService(IMachineController controller)
    {
        _controller = controller;
    }

    public Task MoveToAsync(Position position, CancellationToken cancellationToken) =>
        _controller.MoveToAsync(position, cancellationToken);
}

public sealed class LoggingMotionServiceDecorator : IMotionService
{
    private readonly IMotionService _inner;
    private readonly ILogger<LoggingMotionServiceDecorator> _logger;

    public LoggingMotionServiceDecorator(IMotionService inner, ILogger<LoggingMotionServiceDecorator> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task MoveToAsync(Position position, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Moving to X={X}, Y={Y}", position.X, position.Y);

        try
        {
            await _inner.MoveToAsync(position, cancellationToken);
            _logger.LogInformation("Move completed.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Move failed.");
            throw;
        }
    }
}

Why it is useful

  • preserves single responsibility
  • adds cross-cutting behavior cleanly
  • avoids contaminating business logic with logging/retry code
  • supports composable pipelines

When it is overkill

  • when wrappers stack so deeply that tracing call flow is painful
  • when each decorator adds tiny value but lots of indirection
  • when debugging becomes “which layer threw this?”

Decorator is powerful, but too many wrappers can make simple behavior feel mysterious.


8. Repository

The real problem it solves

At its best, Repository hides persistence details and presents a collection-like access model aligned with the domain.

Examples:

  • loading machine runs
  • saving inspection results
  • querying defect history
  • retrieving recipes

But Repository is also one of the most overused patterns in .NET.

How it appears in .NET systems

csharp
public interface IInspectionRunRepository
{
    Task AddAsync(InspectionRun run, CancellationToken cancellationToken);
    Task<InspectionRun?> GetByIdAsync(Guid runId, CancellationToken cancellationToken);
    Task<IReadOnlyList<InspectionRun>> GetRecentAsync(int count, CancellationToken cancellationToken);
}

Why it is useful

  • isolates persistence from core logic
  • improves testability if used correctly
  • can express domain-centric queries
  • protects application layer from ORM details

Trade-offs and when it hurts

Repository becomes weak when:

  • it is just a thin CRUD wrapper over Entity Framework with no real abstraction value
  • it hides useful query features
  • it duplicates ORM capabilities badly
  • generic repositories distort domain language
  • it creates many tiny methods that nobody can find or maintain

In many modern .NET apps, EF Core already acts like a repository/unit of work combination. Adding another generic repository on top can add no real value.

Repository is useful when:

  • domain access patterns matter
  • persistence source may vary meaningfully
  • you need a stable application boundary
  • you want to prevent query logic from leaking everywhere

Repository is overkill when it just becomes ceremony over DbContext.


9. Mediator / message-based coordination

The real problem it solves

Too many components talk to each other directly, creating a dependency web.

Examples:

  • UI issues command → workflow service → audit logger → notification service → persistence → machine control
  • one action fans out to multiple reactions
  • business operations need orchestration without every component knowing every other

Mediator centralizes coordination or at least reduces direct coupling.

How it appears in .NET systems

Often through libraries like MediatR, but the concept matters more than the library.

Examples:

  • request/response commands and queries
  • domain notifications
  • internal workflow events

Why it is useful

  • reduces direct dependencies
  • clarifies input/output contract of requests
  • supports pipeline behaviors
  • can simplify test setup in some cases

When it is overkill

  • when simple direct service calls would be clearer
  • when every call goes through a mediator “because architecture”
  • when business flow becomes hidden behind handlers distributed across the codebase
  • when tracing the actual execution path becomes difficult

Mediator can either simplify coordination or obscure it. It depends on how far you push it.


10. Template Method vs composition-based alternatives

The real problem it solves

You have a common workflow skeleton with a few varying steps.

Examples:

  • inspection pipeline with fixed phases but machine-specific steps
  • file export process with shared validation and finalization
  • machine startup sequence with vendor-specific commands

Template Method traditionally solves this with inheritance:

  • base class defines algorithm structure
  • subclasses override steps

Why it is less attractive now

In modern .NET, composition often beats inheritance because inheritance tends to:

  • create fragile base classes
  • hide variation through override chains
  • couple unrelated behavior
  • make testing and evolution harder

Composition alternative:

csharp
public interface IAlignmentStep
{
    Task ExecuteAsync(InspectionContext context, CancellationToken cancellationToken);
}

public sealed class InspectionWorkflow
{
    private readonly IReadOnlyList<IAlignmentStep> _steps;

    public InspectionWorkflow(IReadOnlyList<IAlignmentStep> steps)
    {
        _steps = steps;
    }

    public async Task RunAsync(InspectionContext context, CancellationToken cancellationToken)
    {
        foreach (var step in _steps)
        {
            await step.ExecuteAsync(context, cancellationToken);
        }
    }
}

When Template Method still helps

  • stable algorithm skeleton
  • small number of variants
  • controlled inheritance boundary
  • framework-level extension points

But in business and machine logic, composition usually scales better.


Part 3 — Real problems in a wafer inspection WPF system

Now let’s ground this in the actual system:

“A WPF desktop app controlling a wafer inspection machine”

This system naturally attracts certain patterns because of its pressures.

Adapter for vendor SDK integration

This is almost unavoidable.

Vendor SDKs are often:

  • synchronous
  • stateful
  • callback-heavy
  • poorly documented
  • inconsistent across models
  • full of raw codes and structures

You do not want VendorSdkAxisController, VendorAlarmCode, or raw callback signatures spread through ViewModels and application services.

So you build adapters such as:

  • IMotionController
  • ICameraController
  • ILightController
  • IWaferLoader
  • IAlarmReader

This keeps hardware detail at the boundary.

Strategy for inspection or classification variations

Different products, customers, or machine modes often require different behavior:

  • different preprocessing
  • different thresholding logic
  • different classification rules
  • different defect filters
  • different output schemas

You want the overall workflow to remain stable while the variable logic changes cleanly. That is Strategy.

State pattern / state machine for workflow and machine state

This is one of the most important structures in the system.

Because the machine cannot safely do everything at all times.

Examples:

  • cannot start run when not homed
  • cannot load wafer while stage moving
  • cannot clear alarm unless safe state reached
  • cannot export results until inspection finalized

If you do not model state explicitly, invalid operations leak in and the UI becomes inconsistent with actual machine behavior.

Command pattern in WPF

This appears directly in:

  • Start
  • Stop
  • Pause
  • Resume
  • Load recipe
  • Acknowledge alarm
  • Reset machine

The UI binds to commands, and CanExecute reflects machine or workflow state.

This is much better than putting button click logic directly in code-behind.

Observer / event-based result streaming

Inspection systems are event-rich:

  • frame captured
  • defect found
  • batch progress updated
  • health metric changed
  • alarm raised
  • result persisted

These events often feed:

  • live UI
  • historian
  • result storage
  • trace logging
  • notifications
  • monitoring dashboards

A producer-consumer or pub-sub model is a natural fit.

Factory for machine-specific components

Often the same application supports:

  • simulator mode
  • different hardware revisions
  • optional subsystems
  • different camera vendors
  • different deployment configurations

A factory or composition root chooses the right module set.

Decorator for resilience around machine services

You often want wrappers around core services for:

  • logging every machine operation
  • timeout around hardware commands
  • retry for transient communication errors
  • metrics/tracing
  • operator audit logging

You do not want every service method full of repeated try/catch/retry/logging code.

That is where Decorator helps a lot.


Part 4 — How we use these patterns in .NET practically

Let’s connect the patterns into a realistic structure.

Example architecture shape

You might have layers like:

  • UI layer: WPF views, ViewModels, commands
  • Application layer: workflows, orchestration, use cases
  • Domain/application contracts: machine abstractions, result contracts, policies
  • Infrastructure layer: vendor SDK adapters, persistence, file IO, transport
  • Cross-cutting: logging decorators, retry policies, metrics, tracing

Example: prevent hardware details from leaking

Bad version:

csharp
public sealed class MainViewModel
{
    private readonly VendorMachineSdk _sdk;

    public MainViewModel(VendorMachineSdk sdk)
    {
        _sdk = sdk;
    }

    public void StartInspection()
    {
        _sdk.InitializeAxis();
        _sdk.SetMode(4);
        _sdk.ExecuteRecipe("default.rcp");
    }
}

Problems:

  • UI knows vendor API
  • impossible to test meaningfully
  • vendor semantics leak upward
  • hard to change machine model
  • no clean place for retries, logging, timeout, translation

Better version:

csharp
public interface IInspectionWorkflow
{
    Task StartAsync(CancellationToken cancellationToken);
}

public sealed class InspectionWorkflow : IInspectionWorkflow
{
    private readonly IMachineController _machineController;
    private readonly IRecipeLoader _recipeLoader;
    private readonly IInspectionStrategyResolver _strategyResolver;
    private readonly IInspectionContextProvider _contextProvider;

    public InspectionWorkflow(
        IMachineController machineController,
        IRecipeLoader recipeLoader,
        IInspectionStrategyResolver strategyResolver,
        IInspectionContextProvider contextProvider)
    {
        _machineController = machineController;
        _recipeLoader = recipeLoader;
        _strategyResolver = strategyResolver;
        _contextProvider = contextProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        var context = await _contextProvider.CreateAsync(cancellationToken);

        await _machineController.EnsureReadyAsync(cancellationToken);
        await _recipeLoader.LoadAsync(context.RecipeName, cancellationToken);

        var strategy = _strategyResolver.Resolve(context.Mode);
        var result = strategy.Inspect(context);

        await _machineController.CommitInspectionAsync(result, cancellationToken);
    }
}

UI then binds to that workflow through a command.

Example: DI integration

csharp
services.AddSingleton<VendorSdkClient>();

services.AddSingleton<IMachineController, VendorMachineControllerAdapter>();
services.AddSingleton<IRecipeLoader, VendorRecipeLoaderAdapter>();

services.AddScoped<BrightFieldInspectionStrategy>();
services.AddScoped<DarkFieldInspectionStrategy>();
services.AddScoped<IInspectionStrategyResolver, InspectionStrategyResolver>();

services.AddScoped<IInspectionWorkflow, InspectionWorkflow>();

If decorating manually:

csharp
services.AddScoped<MotionService>();
services.AddScoped<IMotionService>(sp =>
{
    var inner = sp.GetRequiredService<MotionService>();
    var logger = sp.GetRequiredService<ILogger<LoggingMotionServiceDecorator>>();
    return new LoggingMotionServiceDecorator(inner, logger);
});

How patterns help testability

Patterns help testability when they create meaningful seams.

For example:

  • Adapter lets you replace hardware with fake implementations
  • Strategy lets you test each behavior independently
  • State logic can be tested as pure transition rules
  • Command logic can be tested through ViewModel state and command enablement
  • Decorator can be tested separately from the core service

Example test seam:

csharp
public sealed class FakeMachineController : IMachineController
{
    public bool EnsureReadyCalled { get; private set; }

    public Task ConnectAsync(CancellationToken cancellationToken) => Task.CompletedTask;

    public Task EnsureReadyAsync(CancellationToken cancellationToken)
    {
        EnsureReadyCalled = true;
        return Task.CompletedTask;
    }

    public Task CommitInspectionAsync(InspectionResult result, CancellationToken cancellationToken)
        => Task.CompletedTask;
}

Now the workflow can be tested without real hardware.

How patterns help extensibility

Suppose a new machine model comes in.

With good boundaries:

  • add a new adapter
  • register a different module through factory/composition
  • maybe add new strategy implementations
  • keep most orchestration unchanged

Without boundaries:

  • vendor-specific branches spread across UI, services, and persistence

That is where patterns pay off.


Part 5 — Common mistakes

Forcing a pattern where a simple class would be enough

This is extremely common.

A team sees future variation everywhere and builds abstractions for everything.

Result:

  • 12 files for a behavior that could be one class
  • interface plus implementation plus factory plus resolver for stable logic
  • more jumping around code than understanding it

Production consequence:

  • slower onboarding
  • slower debugging
  • harder refactoring
  • perceived architecture “cleanliness” but actual lower productivity

Over-engineering with too many layers and abstractions

You can easily end up with:

UI Command → ViewModel service → application service → mediator request → handler → domain service → repository → ORM

for something simple like acknowledging an alarm.

Every layer adds:

  • indirection
  • stack depth
  • debugging cost
  • cognitive load

Layers should exist only where they add a real boundary or responsibility separation.

Using Repository blindly where it adds little value

This is a classic .NET mistake.

If your repository is:

csharp
Task<TEntity> GetByIdAsync(Guid id);
Task AddAsync(TEntity entity);
Task UpdateAsync(TEntity entity);
Task DeleteAsync(Guid id);

for every entity, with almost no domain meaning, it may just be empty ceremony.

Production consequence:

  • duplicated query logic
  • poor use of EF Core capabilities
  • awkward abstractions
  • less readable application code

Building complex inheritance trees instead of composition

Inheritance looks elegant early and painful later.

Typical issues:

  • base class grows too many hooks
  • subclasses override behavior unpredictably
  • shared state becomes fragile
  • testing a subclass requires understanding hidden base behavior

Production consequence:

  • “safe” changes become risky
  • bugs appear from override interactions
  • debugging requires reading parent-parent-parent class hierarchy

Confusing pattern names with design quality

Some code reviews sound impressive:

  • “We use mediator, strategy, repository, decorator, and factory.”

That means nothing by itself.

Bad code can use many patterns. Good code can use very few.

Pattern vocabulary is not architecture quality. Clarity, changeability, correctness, and operational behavior are.

Turning patterns into ceremony

This is when a pattern stops being a helpful structure and becomes ritual.

Examples:

  • every action must be a handler
  • every dependency must have an interface
  • every entity must have a repository
  • every workflow must be abstracted even if only one variant exists

Ceremony creates distance between the developer and the problem.


Part 6 — Trade-offs

Flexibility vs complexity

Patterns often buy flexibility by adding indirection.

That is fine when the variation is real.

It is wasteful when the variation is hypothetical.

Question to ask: What change are we actually preparing for?

If you cannot name a likely change, the flexibility may be imaginary.

Reuse vs readability

A pattern can help reuse, but excessive reuse can make code harder to follow.

Sometimes duplication is cheaper than a deep abstraction.

Especially in machine workflows, code that reads clearly like the physical process is often better than an overly generalized reusable engine.

Abstraction vs debugging difficulty

Abstractions hide details. That is their job.

But debugging production issues often requires seeing details.

Too much abstraction can make it hard to answer:

  • what actually called the device?
  • where did the timeout come from?
  • which handler changed the state?
  • which subscriber reacted to this event?

Senior engineers balance clean abstraction with observable execution.

Framework-driven patterns vs domain-driven patterns

Some patterns are pushed by frameworks:

  • WPF Command
  • DI-driven strategy/decorator composition
  • mediator libraries

Some patterns arise from domain pressure:

  • explicit machine state model
  • adapter around hardware SDK
  • strategy for inspection modes

The strongest designs are domain-driven first. Framework patterns should support the domain, not dictate it.


Part 7 — How to choose a pattern

Recognize the underlying problem first

Do not start with the pattern. Start with the pain.

Ask:

  • What is hard to change?
  • What keeps duplicating?
  • What dependency leaks everywhere?
  • What behavior varies by mode or type?
  • What logic becomes invalid in some states?
  • What cross-cutting concern keeps repeating?

That usually points naturally to a pattern.

Decide whether the pattern is justified

A pattern is justified when it reduces total system complexity over time.

Good signs:

  • recurring variation
  • unstable external dependency
  • repeated cross-cutting behavior
  • invalid state transitions causing bugs
  • clear fan-out event model
  • repeated creation logic by runtime context

When a simpler solution is better

Use the simplest design that still protects the system.

Sometimes that means:

  • one class
  • one enum with explicit transitions
  • one small conditional
  • one direct service call
  • one private helper instead of a factory

Senior engineers are not afraid of simple code.

Patterns often emerge from refactoring

This is a key leadership point.

The healthiest patterns often appear after the code shows a real need.

Example:

  • first version has one inspection flow
  • later two variants appear
  • conditional grows
  • duplication appears
  • then Strategy emerges naturally

That is better than inventing a pattern for ten hypothetical future modes.


Part 8 — Design patterns vs modern .NET features

Modern .NET changes how patterns look.

DI reduces the need for some classic patterns

Constructor injection and service registration already solve parts of:

  • factory management
  • service location
  • dependency assembly

You often do not need elaborate object-creation patterns when DI can compose the graph.

Still, DI does not remove the need for:

  • runtime variant selection
  • boundary isolation
  • explicit workflow/state modeling

Delegates and lambdas simplify Strategy-like cases

Some strategies do not need classes.

Example:

csharp
Func<InspectionContext, InspectionResult> classify = context =>
{
    // simple classification rule
    return new InspectionResult();
};

For small, local variation, this may be enough. For richer behavior, lifecycle, dependencies, or testability needs, a full Strategy class may still be better.

async/await changes observer and command usage

In modern .NET, asynchronous workflows matter everywhere.

Patterns must adapt:

  • commands often need async-aware implementations
  • observers may need channels or async streams
  • decorators must preserve cancellation and timeout semantics
  • adapters often need to bridge blocking SDKs carefully

Records and modern language features reduce boilerplate

Records, pattern matching, and better switch expressions make some designs cleaner and less ceremonious.

For example, state-related decision logic can sometimes be expressed much more clearly now than older OO-heavy code.

Which classic patterns are still highly relevant

Still highly relevant:

  • Adapter
  • Strategy
  • State
  • Command
  • Decorator
  • Factory in contextual creation scenarios
  • Observer / event-driven coordination

Less explicit now:

  • Abstract Factory in textbook form
  • Template Method as a default extension mechanism
  • Singleton as a pattern discussion topic, since DI/service lifetimes usually cover that concern more cleanly

The pattern may still exist conceptually, but modern .NET often expresses it differently.


Part 9 — Code review and design thinking

What a senior engineer looks for

When reviewing pattern usage, a senior engineer asks:

  • What problem is this abstraction solving?
  • Is the variation real or imagined?
  • Does this structure make change easier?
  • Does it improve or worsen debugging?
  • Is the boundary aligned with the domain?
  • Is the code more understandable because of this pattern?

They are not impressed by pattern density. They care about whether the shape of the code matches the pressure of the system.

How to spot when a pattern is helping

A pattern is helping when:

  • related variation is localized
  • new behavior can be added without editing many existing places
  • tests become simpler
  • vendor details stay at boundaries
  • invalid states become harder to represent
  • cross-cutting behavior is centralized cleanly

How to spot when a pattern is hiding complexity

Warning signs:

  • too many tiny classes with weak names
  • call flow hard to trace
  • one business action spread across many handlers/wrappers
  • “indirection for purity” instead of concrete value
  • abstractions that mirror implementation instead of domain concepts

How to challenge unnecessary abstractions constructively

In review, avoid saying:

“This pattern is wrong.”

Better:

  • “What change are we expecting this abstraction to support?”
  • “Would a simpler class be easier to understand here?”
  • “Is this indirection paying for itself yet?”
  • “Could we wait until we have a second variation before extracting this?”
  • “Does this boundary reflect the domain, or just the framework?”

That is how senior engineers guide design without being dogmatic.


Part 10 — Senior engineer mental model

Experienced engineers do not treat patterns as goals. They treat them as tools.

Their mindset is usually:

1. Start from the pressure, not the catalog

They look for:

  • unstable dependencies
  • growing variation
  • repeated workflows
  • invalid state risk
  • cross-cutting duplication
  • fan-out event needs

Then they choose a structure that fits.

2. Use patterns to reduce risk and improve clarity

The real goal is not elegance. The goal is safer change, fewer production bugs, easier testing, and clearer ownership.

3. Let patterns emerge when the codebase needs them

Strong teams refactor toward patterns.

They do not build an architecture museum on day one.

4. Prefer domain clarity over pattern purity

A state machine that clearly models machine readiness is better than a perfectly “textbook” pattern that nobody can follow.

A thin adapter around ugly vendor SDK code is better than leaking the SDK everywhere.

A simple strategy resolver is better than a grand plugin framework no one needs.

5. Explain pattern choices in terms of trade-offs

In an interview, the best answers sound like this:

“I would use an Adapter around the vendor SDK because that boundary is unstable and I want to prevent hardware-specific types and failure semantics from leaking into the application layer.”

“I would use Strategy only where inspection behavior really varies by mode or recipe. I would not abstract everything up front.”

“For machine and workflow behavior, I care more about an explicit state model than about pattern purity, because invalid transitions are a major production risk.”

“I use Decorator a lot for logging, timeout, retry, and metrics because those concerns repeat and I do not want them mixed into the core machine logic.”

That is the voice of someone who has operated real systems.


Final summary

In modern .NET systems, design patterns matter most when they solve real pressure:

  • Adapter isolates ugly or unstable external dependencies
  • Strategy handles controlled behavioral variation
  • Factory centralizes meaningful runtime creation decisions
  • Observer / Pub-Sub supports event-driven flows and streaming
  • State protects workflows with valid transitions
  • Command cleanly connects WPF UI intent to application behavior
  • Decorator adds logging, retry, timeout, metrics, and resilience cleanly
  • Repository can help, but is often overused
  • Mediator can reduce coupling, but can also hide flow
  • Template Method is less attractive now than composition in many cases

The key lesson is this:

Patterns are not signs of good design. They are tools that may help produce good design when applied to the right problem, at the right time, with the right level of simplicity.

For a production WPF machine-control system, the best pattern usage is the one that:

  • keeps hardware boundaries clean
  • makes workflows explicit
  • prevents invalid operations
  • supports observability
  • stays testable
  • remains understandable under production pressure

That is how a senior engineer should think about patterns.

If you want, I can turn this into a second version focused on interview Q&A style answers with likely questions and strong sample responses.

Docs-first project memory for AI-assisted implementation.