Deterministic Core, Non-Deterministic Shell
This piece re-evaluates Gary Bernhardt's 'Functional Core, Imperative Shell' architecture, proposing a 'Deterministic Core, Non-Deterministic Shell' as a more flexible and pragmatic evolution. It argues that determinism, not just functional purity, is key to testability, making the pattern applicable to a wider array of languages and existing codebases. The concept is popular on HN because it offers a practical approach to improving software quality and maintainability in real-world scenarios.
The Lowdown
The article delves into an evolution of Gary Bernhardt's influential 'Functional Core, Imperative Shell' architecture, introducing the concept of a 'Deterministic Core, Non-Deterministic Shell.' This adaptation seeks to broaden the applicability of the original pattern, offering a more flexible approach to designing testable and maintainable software systems, particularly within the constraints of various programming paradigms and legacy code.
- Functional Core/Imperative Shell Defined: The original pattern divides code into a 'Functional Core' (pure business logic, no side effects) and an 'Imperative Shell' (handles state, IO, and external interactions), making the core highly testable due to its isolation and predictability.
- Shift to Determinism: The author argues that the crucial property making the core testable is not strict functional purity, but rather determinism—the guarantee that identical inputs always yield identical outputs. This allows for imperative state machines to also reside in the core if their behavior is repeatable.
- Deterministic vs. Pure Functional: An
AddMachineexample demonstrates how an imperative class can still be deterministic, maintaining the benefits of testability without requiring purely functional programming, thus expanding the pattern's utility. - Non-Deterministic Elements: The article identifies common non-repeatable behaviors—such as unseeded random number generators, asynchronous operations, network communication, and date/time queries—as belonging exclusively to the non-deterministic shell.
- Defragmentation of Determinism: For existing, complex codebases, the article proposes a 'defragmentation' strategy: gradually identifying and isolating deterministic segments of code to create smaller, testable 'cores' within the larger system, even if a single, unified core is unattainable.
- Unleashing State Machines: It encourages developers to seek out and extract inherent deterministic state machines from messy code, emphasizing that this process significantly improves the software's modifiability and testability.
By emphasizing determinism over strict functional purity, the 'Deterministic Core, Non-Deterministic Shell' paradigm offers a pragmatic pathway to enhance the testability, reliability, and maintainability of software. This approach provides a practical framework for both new developments and the daunting task of refactoring legacy systems, ultimately making complex business logic more manageable.