Liking cljdoc? Tell your friends :D

Profile

In production, your deployable projects, by their nature, run in separate processes. In development, you are running from a single REPL process, which can lead to classpath issues you will never encounter in production. Profiles allow you to work on all your projects from the development project without classpath issues.

You likely don’t need profiles if your workspace does not re-specify one interface.

An Example Scenario

In our ongoing tutorial, we last left off exploring testing. If you’ve been following along, your development project currently mirrors the command-line project:

prod mirrors dev

The cli base references the user component in both projects. Let’s pretend that we have discovered performance problems in the user component and have learned that distributing the load by delegating to a new service should solve the problem:

new service in prod

The cli base now references a new user-remote component, which calls a new user-api base in a new user-service project, which references our original user component.

The production environment looks good, but how about the development environment? We have a problem. The user and user-remote components both specify the user interface, resulting in two se.example.user.interface namespaces on the classpath. Duplicate items on the classpath will confuse classloaders and IDEs.

Classloader issues are only a development project concern. We don’t have classloader issues:

  • in production because command-line and user-service projects naturally run in separate processes.

  • when testing via poly because test-runners provide classpath isolation at the project level.

Profiles to the Rescue

The poly solution is to use profiles:

profiles in dev

We can add default and remote profiles to avoid duplication on the classpath, allowing us to develop all our code and projects from a single place without issues.

If no other profiles are selected, poly automatically merges the default profile into the development project.

The name default is specified by :default-profile-name in workspace.edn.

Planning a Solution

Let’s move from our current design:

prod mirrors dev

…​to our new one:

target design

Implementation

Let’s continue with our tutorial example workspace; we last left off exploring testing.

First, we need some mechanism for the command-line tool to communicate over the wire with the user-service. After some searching, we found the slacker library. It supports simple remote procedure calls. Perfect for our tutorial.

Here’s a checklist to implement our design:

1. Create the user-api base

a. Create the user-api base

Run:

poly create base name:user-api

b. Add the slacker library to the user-api base

Edit user-api base deps.edn:

./bases/user-api/deps.edn
{:paths ["src" "resources"]
 :deps {slacker/slacker {:mvn/version "0.17.0"}} (1)
 :aliases {:test {:extra-paths ["test"]
                  :extra-deps {}}}}
1Add slacker library dependency for server-side communication support

c. Add user-api base to ./deps.edn:

Adjust ./deps.edn like so:

./deps.edn
 :aliases  {:dev {:extra-deps [...
                               poly/user-api {:local/root "bases/user-api"} (1)
                               ...]

            :test {:extra-paths [...
                                 "bases/user-api/test" (2)
1Add user-api source dep under :dev alias
2Add user-api test path under :test alias

d. Implement the server for the user-api base

Create the api namespace in the user-api base:

example
├── bases
│   └── user-api
│       └── src
│           ├── se.example.user_api.api.clj (1)
│           └── se.example.user_api.core.clj
1Create the new api.clj file

Set the content of api.clj to:

./bases/user-api/src/se/example/user_api/api.clj
(ns se.example.user-api.api
  (:require [se.example.user.interface :as user]))

(defn hello-remote [name]
  (user/hello (str name " - from the server")))

Update core.clj to:

./bases/user-api/src/se/example/user_api/core.clj
(ns se.example.user-api.core
  (:require [se.example.user-api.api]
            [slacker.server :as server])
  (:gen-class))

(defn -main [& args]
  (server/start-slacker-server [(the-ns 'se.example.user-api.api)] 2104)
  (println "server started: http://127.0.0.1:2104"))

2. Create the user-remote component

a. Create the user-remote component

Run:

poly create component name:user-remote interface:user

b. Add the slacker library to user-remote component

Edit user-remote component deps.edn:

./components/user-remote/deps.edn
{:paths ["src" "resources"]
 :deps {slacker/slacker {:mvn/version "0.17.0"}} (1)
 :aliases {:test {:extra-paths ["test"]
                  :extra-deps {}}}}
1Add slacker lib dependency for client-side communication support

c. Remove the user component from ./deps.edn:

./deps.edn
{:aliases  {:dev {...
                  :extra-deps {poly/user {:local/root "components/user"} (1)
                               poly/cli  {:local/root "bases/cli"}
                               poly/user-api {:local/root "bases/user-api"}

                               org.clojure/clojure {:mvn/version "1.12.0"}}}

            :test {:extra-paths ["components/user/test" (2)
                                 "bases/cli/test"
                                 "projects/command-line/test"
                                 "bases/user-api/test"]}
1Delete poly/user {:local/root "components/user"}
2Delete "components/user/test"

d. Add the default and remote profiles to ./deps.edn:

./deps.edn
:aliases  {...

           :+default {:extra-deps {poly/user {:local/root "components/user"}} (1)
                      :extra-paths ["components/user/test"]}

           :+remote {:extra-deps {poly/user-remote {:local/root "components/user-remote"}} (2)
                     :extra-paths ["components/user-remote/test"]}
1Respecify your deleted user component under the default profile alias
2Specify your new user-remote component under the remote profile alias

Notice that profile aliases are prefixed with a +.

e. Activate the remote profile in your IDE

At the time of this writing, we only have instructions for Cursive.

Cursive users: Activate the remote profile in your IDE:

activate remote profile

f. Implement user-remote

Create the core namespace in the user-remote component:

example
├── components
│   └── user-remote
│       └── src
│           ├── se.example.user.core.clj (1)
│           └── se.example.user.interface.clj
1Create new core.clj file

Set core.clj content to:

./components/user-remote/src/se/example/user/core.clj
(ns se.example.user.core
  (:require [slacker.client :as client]))

(declare hello-remote)

(defn hello [name]
  (let [connection (client/slackerc "localhost:2104")
        _ (client/defn-remote connection se.example.user-api.api/hello-remote)]
    (hello-remote name)))

And update the interface.clj content to:

./components/user-remote/src/se/example/user/interface.clj
(ns se.example.user.interface
  (:require [se.example.user.core :as core]))

(defn hello [name]
  (core/hello name))

g. Activate the default profile in your IDE

At the time of this writing, we only have instructions for Cursive users.

Cursive users: Edit the REPL configuration:

edit repl config

…​and add the default profile to Options: -A:dev:test:build:+default

We had you add -A:dev:test during initial setup. Alternatively, you could have initially added -A:dev:test:build:+default. Tools.deps ignores unused aliases. The extra alias, while unused (until now), would have been harmless.

We now need to include the +default alias because we moved the user component from a default tools.deps dependency to a default polylith dependency.

We have segregated the two components that specify a user interface via profiles. You might wonder why we chose the user component to be in the default profile and user-remote in the remote profile. Our rationale is that we wanted something simple by default. The user component only communicates in-process, whereas the user-remote component communicates out-of-process over-the-wire.

For the changes to take effect, you need to restart the REPL. Normally, a REPL restart is not required, but when adding profiles, it’s necessary.

3. Switch from user to user-remote in the command-line project

a. Replace user with user-remote in command-line project

Make the following changes to the command-line project deps.edn:

./projects/command-line/deps.edn
{:deps {poly/user {:local/root "../../components/user-remote"} (1)
        poly/cli  {:local/root "../../bases/cli"}

        org.clojure/clojure {:mvn/version "1.12.0"}
        org.slf4j/slf4j-nop {:mvn/version "2.0.9"}} (2)

 :aliases {:test {:extra-paths ["test"]
                  :extra-deps  {}}

           :uberjar {:main se.example.cli.core}}}
1Rename components/user to components/user-remote. It’s okay to leave poly/user as is; it’s unique within the project.
2Add logging library (slacker lib does some logging that we’ll ignore)

b. Create command-line uberjar

Run:

clojure -A:deps -T:build uberjar :project command-line

4. Create the user-service project

a. Create the user-service project:

Run:

poly create project name:user-service

b. Configure the user-service

Set the user-service project deps.edn content to:

./projects/user-service/deps.edn
{:deps {poly/user {:local/root "../../components/user"} (1)
        poly/user-api {:local/root "../../bases/user-api"} (2)

        org.clojure/clojure {:mvn/version "1.12.0"}
        org.slf4j/slf4j-nop {:mvn/version "2.0.9"}} (3)

 :aliases {:test {:extra-paths []
                  :extra-deps  {}}

           :uberjar {:main se.example.user-api.core}}} (4)
1Add user component
2Add user-api base
3Add logging library (slacker lib does some logging that we’ll ignore)
4Specify main for uberjar artifact

c. Add a poly alias for the user-service

./workspace.edn
 :projects {"development" {:alias "dev"}
            "command-line" {:alias "cl"}
            "user-service" {:alias "user-s"}}} (1)
1Add user-s alias for your new user-service

5. Build the user-service

Create an uberjar for the user-service:

clojure -A:deps -T:build uberjar :project user-service

Verifying our Work

Phew, that should be it! Now, let’s test if it works.

From a separate terminal, launch the user-service:

cd projects/user-service/target
java -jar user-service.jar

You should see the following output:

server started: http://127.0.0.1:2104

Cursive users: Now that you have a running service, you can test if you can call it from the REPL. You activated the remote profile in your IDE earlier, which made the user-remote component active.

Note that this only instructs the IDE to treat user-remote as source code:

user and user remote

…​but it doesn’t automatically load its source code into the REPL!

You can verify this by adding this code to development/src/dev/lisa.clj:

(ns dev.lisa
  (:require [se.example.user.interface :as user]))

(user/hello "Lisa")

…​and if you execute the hello function, you will see that the loaded user component is called (not the user-remote component):

"Hello Lisa!!"

Remember, you set your REPL configuration to include the default profile. Because you configured the user component to be in the default profile, it will get loaded every time you start or restart your REPL. As mentioned earlier, we typically recommend placing your simpler component in the default profile.

Let’s create a REPL configuration that includes the remote profile:

prod repl

This REPL will use the user-remote component and can be used to emulate a production-like environment.

But let’s continue with the REPL that is already running and see if we can switch to user-remote without restarting the REPL.

Open the core namespace of the user-remote component and select Tools > REPL > Load file in REPL. You have just replaced the user component implementation with user-remote, which works because both share the same se.example.user.core and se.example.user.interface namespaces.

If you execute the hello function again from dev.lisa, you should see:

Hello Lisa - from the server!!

Now, let’s continue with our example. From another terminal (not the one from which you started the user-service) from your example workspace root dir:

cd projects/command-line/target
java -jar command-line.jar Lisa

You should see:

Hello Lisa - from the server!!

If your output matches, congratulations, you’ve successfully exercised poly profiles!

You can find the complete tutorial code here.

Now execute the info command (+ deactivates all profiles, and makes the default profile visible):

cd ../../.. (1)
poly info +
1Navigate back to the workspace root dir

…​and compare the info output with our target design:

compare with target design annotated

Great! Reality now matches our plan!

Notice that profile flags only include the st flags and never the x flag. Whether or not to run tests is not tied to profiles.

This example was quite simple, but if your project is more complicated, you may want to manage state during development with a tool like Mount, Component, or Integrant. You could also create your own helper functions in your development project namespace (dev.lisa, in our tutorial) to help you switch profiles with a library like tools.namespace.

Exploring with poly info

By default, the default profile is active:

poly info
info after adding profiles annotated

Notice:

  • default is listed for active profiles

  • the dev project column:

    • includes the user brick (which is in the default profile)

    • doesn’t include the user-remote brick (which is in the remote profile)

  • columns for the inactive remote profile are shown

Profiles can also contain dependencies and paths to projects, but we’ve done no such thing in our example; therefore, you’ll see all profile flags as -- in the project section.

You can override the default profile by specifying a profile:

poly info +remote
info with remote profile annotated

Notice:

  • remote is listed for active profiles

  • that the dev project column:

    • doesn’t include the user brick (which is in the default profile)

    • includes the user-remote brick (which is in the remote profile)

  • columns for the inactive default profile are shown

You can specify more than one profile:

poly info +default +remote
info multiple profiles annotated

Notice:

  • default and remote are listed as active profiles

  • that the dev project column:

    • includes the user brick (which is in the default profile)

    • includes the user-remote brick (which is in the remote profile)

  • no inactive profile columns are shown

  • poly tells us that it does not like that we included both user and user-remote in the development project

Let’s see how many lines of code we have by specifying the :loc argument:

poly info :loc
info loc

Under bricks, each project column tallies the lines of code for its bricks src code. The loc column counts the number of lines of codes for src directories, while (t) counts for the test directories.

Our tutorial example is small, but your real-world systems will likely reach thousands of lines of code. When that happens, you may want to reconfigure the thousand delimiter, which is , by default.

Testing with poly test

Let’s run all the tests to verify that everything works:

poly test :project
test

If your output matches, all that green is a very good sign; pat yourself on the back!

Can you improve this documentation? These fine people already did:
Joakim Tengstrand & Sean Corfield
Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close