In general, FP models things as mappings from input to output, in a sort of dataflow style. It’s more important in the small. So you want functions that only depend on their arguments, and that do not modify anything outside of them. All they do is read from their input arguments, and generate the appropriate output return from them.
This is where most of the difference will be as opposed to the traditional procedural code, like you’d have in Java or C++/Python, where methods almost always manipulate external state such as object fields, global vars, static fields, or even their input arguments pointed value (they mutate their input args).
So when you get to the small design choices, you’ll need to try really hard to follow this FP style, and write most of your computations as taking the current state as input and returning a modified version as output.
Now, in the medium, things don’t look very different then in OO. What happens is that, the part of the code which updates the global state is no longer inside the functions that transform it. Instead, it is inside the calling function. Ideally, the calling function is an orchestrator of state changes and exist at the top most level, so it’s the boundary of the app.
Imagine that top layer as only being allowed to move data around. It can’t actually do anything else. It can extract data from IO or globals, pass it back and forth to other functions and it can write it back to IO or globals. All data transformation happen in your pure FP functions.
That leaves the choice of where and how to store the global state up to you. You could use a real DB, SQL or NoSql, like say DynamoDB, MongoDB, MySQL or PostgresSQL. Or you could use an in-memory one like SQLite, H2, or DataScript. Or you can go with simpler things, like plain old Clojure Maps, Vectors, Sets stored inside mutable references in global Vars such as Atom or Refs. In any case, how you model the data is up to you. For example, @linpengcheng posts shows a way to model data in a relational way using Clojure maps. More conventionally, you can model it hierarchically as well, and use Spec on top to define the data model.
Records in Clojure are normally used for polymorphic type dispatch or really performant field access. If you don’t need any of those, use normal Maps instead with a Spec for defining their schema. It’s simpler and more flexible.
Now, there’s one other way to manage the state, it’s a lot harder, but more functional. That is through recursion instead of mutation. This allows you to make the orchestrator pure as well (minus any sort of IO required). Basically, the orchestrator would take in the state, which it extracts things from, pass it back and forth to the pure FP functions in the order you need it, but it doesn’t update the state, instead it reconstructs a new one, and when it is done, it calls itself or the next orchestrator and passes in that new state. In Clojure, you’d need to use Loop/Recur and Trampoline not to blow up the stack and get TCO.
A simple model of this would be to have a top level recursive event loop. It waits for commands from IO, and based on the command, it calls the appropriate orchestrator for it, passing into it the state. The orchestrator when done returns it the new version of the state, and the event loop recurs, calling itself back with the new state, and then waits again on IO for the next command, rinse and repeat.