Struggling with Clojure web server structure

Please allow me to introduce a hypothetical situation.

I am writing a web server for a restaurant booking system. I am deciding, firstly, how best to structure my code because I am adverse to libraries such as Component or Mount - my understanding is that these are designed for stateful components, which I don’t intend to keep. I have nouns like Customer, Table, Restaurant, Waiter.

I am concerned about introducing cycling dependencies with code like

;; customer.clj
(:require [app.table :as table])

(defn has-booking? 
  [c]
  (table/by-customer c))

;; table.clj
(:require [app.customer :as customer)

(defn book! 
  [t  c]
  (if (customer/has-booking?)
      (fail :customer-is-already-booked-on-different-table)
      (save-booking! t c))

I’ve been advised to use “interfaces” (protocols) along the lines of

;; interfaces.clj
(defprotocol ICustomerManager
  (has-booking? [this customer]))

(defprotocol ITableManager
  (book! [this table customer]))

However, this scares me because it doesn’t look very functional. Also, without any kind of ‘system’ manager (like Component or Mount), how do I instantiate and then distribute my instances of these components?

How should I move forward?

Thanks

Good question, and something that wasn’t very obvious to me either when I was thinking about this :slight_smile:

The code examples that you gave are somewhat non-idiomatic. Not in the way the code is written, but in the way that it’s not clear if they are pure functions or if they go to some database or both.

If you’re using pure functions that operate on data structures, then usually arranging those in such a dependency tree that you get around cyclic dependencies works fine.

There would be an extra layer that would load state from db and write state from db. This is very DB-dependant. This layer is also easy to arrange into dependencies.

Then there would be a layer that ties everything together. This is usually the layer that imports everything else, but usually cyclic imports don’t enter into this.

My advice would be to start writing code and see what happens. You don’t need, for example, to make namespaces like app.table and app.customer. Why not app.bookings? Remember, within a namespace, you can always have cyclic dependencies on vars by using declare.

1 Like

Component etc are useful for managing things like web servers and database connection pools, so they can be setup, passed around, and torn down, both in your production code and in your test code. Specifically things that have a start/stop lifecycle. At work we use them for connection pools, Jetty servers, caches, Redis pools, configuration (start means load/initialize the configuration), etc. Component manages the order of start/stop for you, based on the dependencies you declare between those things (database depends on configuration, for example).

You are right that Component etc are not intended for your domain entities, but they are good for services that require setup/teardown and have dependencies on other services.

You’re over-complicating it :stuck_out_tongue:

First off, why do you have these capital letter nouns? Like Customer, Table, Restaurant?

This is OOP mindset. In Clojure, you need to think in terms of data-structure. You don’t need protocol, you don’t need objects, just plain data.

The first question is, where is your data stored? I am going to assume it is stored in some relational database for now. With the following table schemas:

table: table-id, chair-count
customer: customer-id, phone-number
restaurant: restaurant-id, restaurant-name, table-ids, waiter-ids
waiter: waiter-id, waiter-name
bookings: restaurant-id, customer-id, table-id, waiter-id

That’s your data model. Once you’ve figured that out, in Clojure, you just use it directly, no extra ORM layer or anything like that.

(defn book!
  [db restaurant-id customer-id]
  (let [available-tables (get-available-table db restaurant-id)]
    (when available-tables
      (let [new-booking (make-new-booking restaurant-id, customer-id, available-tables)]
        (save-booking! db new-booking)))))

This is really simplified to give you an idea, you’d want a transaction, you’d want to factor out some of this logic into multiple function, you need to make the queries to the DB, you’d need logic to know how many chairs for the booking, and find a table that best fits, etc.

But its to show you the idea, you just query the DB, get the data back, in the shape of your data model, and you just perform the necessary verification and modification on it. Done.

You don’t need to create any more abstraction in your code.

This does not matter, shove it all in one giant namespace called: my-cool-booking-app.core. I am not sarcastic :stuck_out_tongue: I’m serious. Let me explain.

In Clojure, the structure of the code into namespaces is only for code readability and discoverability. It is why you would maybe want to store your music collection into folders. The structures that matters in Clojure are the functions and the data-structures.

You asking this question is because of your OOP background. In OOP, classes are a unit of abstraction, they are a structural unit, and sometimes they map with files. What you are asking is how to structure your classes, but there are no classes in Clojure, so this is the mental blocker you are facing.

Namespaces in Clojure are best compared to packages in Java. Why do you have packages in Java? They don’t really matter, you could shove all your classes under one package, or choose to split it up, its purely organizational.

Structurally, what you want to do in Clojure is define what are the functions you want, and the data-structures they’re going to work with. The answer to this is often, well, what is my DB, and what is my DB data model? Whatever your DB is and its data-model, those are the data-structures you work on directly. Now just go and write functions that thus query and manipulate the result-set directly.

9 Likes

Thanks for this answer. You guessed correctly, I come from an OOP background and still trip up when thinking about things in terms of data rather than classes.

1 Like

I suggest you watch this talk to get a better grasp on thinking functionally: Thinking the Clojure Way

1 Like

This topic was automatically closed 182 days after the last reply. New replies are no longer allowed.