A programming paradigm is a model for organizing state and control flow. It determines which unit carries behavior, how larger operations compose, and where side effects appear. C# supports several of these models in the same program. An order can be an object with guarded invariants, its lines can pass through pure transformations, and the completed transaction can emit an event.

The practical test is visibility: which model makes the state transition and its effects easiest to inspect? Software Design/Paradigms/OOP fits behavior tied to identity and invariants. Software Design/Paradigms/Functional Programming fits deterministic transformations. Event-driven code fits work triggered by facts or signals, while imperative code remains the clearest choice for a short, explicit sequence.

Programming Paradigms by State, Control, Effects, and Concurrency

StyleControl flowState modelComposition unitEffectsConcurrency semanticsRepresentative support
ImperativeStatements execute in an explicit orderUsually mutable variablesProcedure or methodPerformed inlineMust be coordinated explicitlyC#, Go, C
Object-orientedCalls dispatch through objects and interfacesEncapsulated behind object methodsObject and interfaceOwned by collaborating objectsSynchronization follows shared object stateC#, Java, Smalltalk
FunctionalExpressions transform valuesPrefer immutable valuesFunctionIsolated at boundariesImmutable values reduce shared-state coordinationF#, Haskell, C# with LINQ
Logic/declarativeState the result or constraints, not the stepsEngine-managed facts or relationsRule, query, or expressionDelegated to the runtimeDefined by the query or rule engineProlog, SQL
Event-drivenA producer publishes an event. Registered handlers run according to the runtime or brokerSubscriber state and event-derived projectionsEvent and handlerAt publication and handler boundariesDelivery and ordering depend on the runtime. Handlers may still run synchronously and block.NET events, message brokers, UI event loops
Reactive streamsValues flow through operators after a subscription establishes demandStream state and accumulated projectionsStream operatorAt subscription and terminal-observer boundariesDemand and backpressure are explicit only when the chosen protocol supports themReactive Streams, IAsyncEnumerable<T>, Rx operators
ConcurrentSeveral tasks make progress over overlapping timeShared, isolated, or message-passedTask, actor, or channelCoordinated across tasksProgress can interleave even on one coreC# tasks/channels, Erlang actors, Go goroutines

Event-driven code moves control to handlers when an event occurs. Delivery may still be synchronous: ordinary C# events invoke handlers on the publishing thread unless the handler starts other work. Reactive streams describe values flowing through a stream contract. Backpressure is present only when that contract exposes demand or otherwise bounds production against consumption. The two styles often meet in one design, but they solve different problems.

Concurrency allows operations to make progress during overlapping periods. Parallelism executes work simultaneously, usually across cores. An asynchronous HTTP operation provides concurrency because its thread can return to the pool while I/O is pending. A CPU-bound Parallel.For becomes parallel when iterations run at the same time. The distinction determines whether a design needs shared-state synchronization, capacity limits, or both.

Imperative, Functional, and Object-oriented Styles

All three examples reject negative invoice lines and total the rest. The result is identical. The ownership of state and behavior changes.

static decimal TotalImperative(IEnumerable<decimal> amounts)
{
    var total = 0m;
 
    foreach (var amount in amounts)
    {
        if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amounts));
        total += amount;
    }
 
    return total;
}

The imperative version exposes both the sequence and the accumulator. It is easy to step through. Correctness still depends on every mutation path preserving the rule.

static decimal TotalFunctional(IReadOnlyList<decimal> amounts) =>
    amounts.Any(amount => amount < 0)
        ? throw new ArgumentOutOfRangeException(nameof(amounts))
        : amounts.Sum();

The functional version expresses validation and reduction as transformations. No mutable accumulator escapes the function, and the same values produce the same result.

public sealed class Invoice
{
    private readonly IReadOnlyList<decimal> _amounts;
 
    public Invoice(IReadOnlyList<decimal> amounts)
    {
        if (amounts.Any(amount => amount < 0))
            throw new ArgumentOutOfRangeException(nameof(amounts));
 
        _amounts = amounts.ToArray();
    }
 
    public decimal Total() => _amounts.Sum();
}

The object-oriented version protects the invariant at construction and keeps behavior beside the state. The type earns its keep when an invoice has identity and several legal transitions. For a one-off calculation, it adds machinery without adding much clarity.

QuestionImperativeFunctionalObject-oriented
Where is state?Local mutable accumulatorInput and derived valuesPrivate object fields
What composes?Statements and proceduresFunctionsObjects and interfaces
Where is the invariant checked?In the procedureAt the transformation boundaryAt construction and methods
Best fitShort explicit workflowsData pipelines and calculationsDomains with identity and legal transitions
Main costMutation paths grow hard to trackEffect boundaries need disciplineTypes and indirection can outgrow the problem

References

3 items under this folder.