A programming paradigm is a set of choices about where state lives, how control moves, and what unit composes into a larger program. C# is multi-paradigm: a service can model an order as an object, transform its lines with pure functions, react to events, and coordinate concurrent work without committing the entire codebase to one style.

The useful question is not “which paradigm wins?” It is “which model makes the state transitions and effects easiest to see?” Use OOP when identity and invariants dominate, Functional Programming for deterministic transformations, event-driven code when control should follow events, and imperative code when an explicit sequence is the clearest description.

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 describes how control is triggered and how producers are decoupled from handlers. It does not imply asynchronous or non-blocking delivery: a C# event invokes its handlers synchronously unless the handler explicitly starts asynchronous work. Reactive streams describe value flow through a stream protocol; backpressure exists only when that protocol carries demand or provides a bounded consumption mechanism. These choices can combine, but they answer different questions.

Concurrency is about overlapping progress; parallelism is about simultaneous execution on multiple cores. An async HTTP request is concurrent while the thread is free to do other work, even if no two instructions run at once. A CPU-bound Parallel.For is parallel when iterations execute on different cores. Treating the terms as synonyms leads to the wrong synchronization and capacity assumptions.

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 the sequence and accumulator. It is the easiest version to step through, but correctness 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. The caller owns no mutable accumulator, which makes the function deterministic for the same input.

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 once and keeps the behavior beside the state. That extra type pays off when an invoice has identity and more legal transitions; it is ceremony when the operation is a one-off transformation.

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

5 items under this folder.