Functional Programming (FP) and the SOLID principles originate from different paradigms—FP focuses on immutability, pure functions, and declarative composition, while SOLID principles were designed to guide Object-Oriented Programming (OOP). However, many of these principles have counterparts in FP and can still enhance software design when applied appropriately.

In this article, we explore how FP naturally aligns with and reinterprets the SOLID principles, using practical examples.

1. Single Responsibility Principle (SRP) in FP

Principle: A module, function, or process should have only one reason to change.

How FP Achieves It:

  • FP naturally promotes small, pure functions that focus on a single operation.
  • Function composition allows complex behavior to be built from simple, single-responsibility functions.

Example:

(defn parse-json [input]
  (clojure.data.json/read-str input :key-fn keyword))
 
(defn validate-data [data]
  (and (:name data) (string? (:name data))))
 
(defn save-to-database [data]
  (println "Saving:" data))
 
(defn process-data [input]
  (let [parsed (parse-json input)]
    (when (validate-data parsed)
      (save-to-database parsed))))

Each function does one thing well, following SRP naturally.


2. Open-Closed Principle (OCP) in FP

Principle: Software should be open for extension but closed for modification.

How FP Achieves It:

  • Higher-order functions and function composition allow behavior to be extended without modifying existing code.

Example:

(defn discount-price [price discount-fn]
  (discount-fn price))
 
(def no-discount identity)
(defn ten-percent-discount [price] (* price 0.9))
(defn seasonal-discount [price] (* price 0.8))
 
(println (discount-price 100 ten-percent-discount)) ; 90.0
(println (discount-price 100 seasonal-discount))   ; 80.0

New discount strategies can be added without modifying discount-price.


3. Liskov Substitution Principle (LSP) in FP

Principle: Subtypes should be substitutable for their base types without altering correctness.

How FP Achieves It:

  • FP prefers data-driven polymorphism and algebraic data types over inheritance.

Example:

(defmulti process-payment :type)
 
(defmethod process-payment :credit-card [payment]
  (println "Processing credit card:" (:number payment)))
 
(defmethod process-payment :paypal [payment]
  (println "Processing PayPal:" (:email payment)))
 
(process-payment {:type :credit-card :number "1234-5678-9012-3456"})
(process-payment {:type :paypal :email "user@example.com"})

Since all cases are explicitly handled, we avoid runtime substitution issues.


4. Interface Segregation Principle (ISP) in FP

Principle: Clients should not be forced to depend on methods they do not use.

How FP Achieves It:

  • Functions in FP are inherently granular, avoiding bloated interfaces.
  • Instead of large interfaces, FP promotes function composition and fine-grained function design.

Example:

(defn send-email [email message]
  (println "Sending email to" email ":" message))
 
(defn send-sms [phone message]
  (println "Sending SMS to" phone ":" message))

Clients only depend on the functions they need, avoiding unnecessary dependencies.


5. Dependency Inversion Principle (DIP) in FP

Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions.

How FP Achieves It:

  • Functions can accept dependencies as parameters (Dependency Injection via function arguments).
  • Higher-order functions and composition promote decoupling.

Example:

(defn log-to-console [message]
  (println "LOG:" message))
 
(defn log-to-file [message]
  (spit "log.txt" (str message "\n") :append true))
 
(defn process-transaction [amount log-fn]
  (log-fn (str "Processing transaction of $" amount)))
 
(process-transaction 100 log-to-console)
(process-transaction 200 log-to-file)

The logging mechanism is injected as a function parameter, keeping process-transaction decoupled from logging details.


Conclusion

While SOLID principles were originally designed for OOP, functional programming achieves similar goals through small, pure functions, composition, and higher-order functions. By embracing these FP techniques, developers can write maintainable, scalable, and decoupled code—without the overhead of traditional OOP design patterns.

Would you like to see more examples or a deep dive into any particular principle?