Does it make sense in 2018 to write microservices in Clojure?
What Clojure has to offer to the microsevices ecosystem?
What are the pros and cons of writing microservices in Clojure?
Does it make sense in 2018 to write microservices in Clojure?
I think it makes sense to structure your project out of small independent components the way you would with microservices. However, I see very little point in actually breaking a project up into separate processes. The main reason you end up doing that in imperative languages is to enforce boundaries between components. However, immutable data structures already facilitate low coupling by design.
Having separate processes introduces additional runtime overhead since you have to copy data between them, and it’s more difficult to orchestrate multiple interdependent services. With Clojure you can safely use threads by leveraging immutable data and concurrency safe primitives like atoms.
The only reason I can see for breaking things up into separate processes is in cases where you’d want to be able to control the lifecycle of different services independently of each other.
My question is not about whether microservices are desired or not.
My question deals with cases where we have already decided we want microservices (for instance because each microservice has its own scalability requirements or because each microservice is managed by a different team or any other good reason). In that case, why would it be a good choice (or not) to write the microservices in Clojure?
Ah I see, in that case I’d say it depends on whether startup time is important and if you’re OK having more memory overhead per service. We ended up going with ClojureScript for this sort of stuff at work. We’ve got a few standalone services, such as PDF generation, that we found were a good fit for the microservice model. At the time, ClojureScript on Node had a much smaller footprint, and much better startup times than Clojure on the JVM. Older JVMs also didn’t play nice with memory management in Docker containers. With JDK 10+ looks like the memory footprint issue has mostly been addressed in my opinion, but you’re still looking at slower startup times.
I ended up publishing Macchiato that mirrors Ring API on top of Node. It makes it relatively painless to move services that don’t have a lot of native dependencies between Node and the JVM. We’ve ended up moving some of our Node services to the JVM as their scope grew, and that worked out pretty well for us. YMMV though.
Another promising option going forward is using GraalVM, but it’s still fairly immature at the moment and you might run into trouble depending on what Java libraries you rely on.
You might want to take a look at this as well: https://medium.com/formcept/fnproject-clojure-goes-serverless-d65a1b2fbbcb
Anyway I’m not really sure why there’s a startup time issue: if I have a service running 24/7 in its own environment the JVM will be already spinning when the service has to run
And I didn’t know about Macchiato! It seems really cool!
Yeah it depends on how you run the services. For long running services it’s obviously not an issue, but for something like AWS Lambda it’s a bit of a problem.
A programming language isn’t really relevant at the architechture level for microservices, since by design, microservices are made to enable polyglot programming and use language agnostic protocols.
Same as for writing any other kind of application.
- It’s a clean simple elegant language where most things generally end up needing less code than in other languages, boosting productivity as well as lowering the possibility of bugs.
- It enforces good safety through immutability, higher level looping and data manipulation, safe concurrency constructs, and functional programming mantras of striving for purity with well compartimentalized side effects.
- It’s performant, in the league of Java, and enforces you to write performant code by giving you a full suite of highly optimized data structures, and only including core functions over them which don’t degrade to O(n) or worse complexities. It also has great support for fast multi-threading.
- It’s mostly secure, usage of immutability, protections against overflows, avoidance of off by one errors, JVM offers many paid support for zero day patches, etc. Though at an application level, it doesn’t do much to help you with security, that’s still up to you and why I say it is mostly secure.
- It enables REPL driven development, pseudo-live coding, which increases productivity and feedback loop, and helps to highly test code.
- It has baked in support for unit tests, generative tests, and runtime contract validation. Which help build robust, functionally correct software, and helps avoid future regressions.
- It comes with a battery included standard library and has access to the entire JVM ecosystem of libraries. Meaning you’ll always find a function, library, or framework for what you want. Increasing productivity, but also reliability, by using existing code that has been tested by others in real production scenarios.
- It has a little sibling called ClojureScript, which allows the same code (mostly), to be shared or re-used for front end development, scripting, quick boot up use cases, command line tools, etc.Thus your investment in Clojure will pay off and apply beyond use cases well suited to the JVM.
- Its syntax and compiler are highly extendable, through macros and reader literals, enabling DSLs which can help boost productivity and readability, as well as adding compile time evaluation if need be.
- It supports a very well thought out set of abstractions that work well together and really help you design code that is easy to evolve iver time, reducing the need for long refactors, thus reducing the maintenance burden of apps.
- It embraces a philosophy of never breaking backwards compatibility, once again massively reducing the maintenance burden. This also contributes to enhanced security, since it should be very cheap and quick to upgrade versions of the JVM and Clojure to keep up with latest security patches.
- It has its own convenient first class object notation, which is superior to JSON, yet just as easy to use from Clojure called EDN. Enabling simple human readable serialization.
- It is not memory efficient, increasing the cost of hardware by requiring high memory servers.
- It has a steep learning curve, but it’s just an initial hump, that said, the hump can discourage developers and make finding low cost developers harder. For example, the stackoverflow survey said Clojure devs were on average the most highly paid. Which might indicate as a company, you’ll have to offer higher salaries to attract Clojure talent.
- It can not be used for real time or soft real time services, preventing it from being useful for some use cases which cannot compromise on responsiveness.
- It lacks a highly reliable static typing layer (it has core.typed, but it’s a one man job, and thus cannot be considered highly reliable). This can be argued to make it harder for big teams to work on shared code bases, as well as making refactorings trickier. It also puts a higher burden on tests.
- Its error messages require a little bit of deciphering to pin point exactly where and what is causing the error.
- It cannot be used for highly performant low level code, the likes which would need C, C++, Rust or Assembly to achieve. That is, it’s capped at Java’s levels.
- It cannot easilly leverage low level code and APIs, that is, it has no easy way to bind to C, or to inline Assembly, etc. It’s possible, but not easy.
- It is a large language, and can take a while to fully master. It offers many ways to do many things, with its philosophy to never break anything, it has accrued a lot of features over time. Navigating them all can be confusing.
- It requires learning a new programming syntax, and potentially new editors, such as Emacs, which are based on unconventional UX paradigms.
- Its artifacts are big in size. Newer JVM versions might remedy this by allowing modularized artifacts to exclude code that your service doesn’t use.
- Its boot times is pretty slow. Preventing it from being a perfect choice for some use cases such as AWS lambdas or other serverless scenarios. Though newer JVMs, such as GraalVM might remedy this. ClojureScript could also be used instead.
Thanks for your detailed answer @didibus.
Although I feel that the pros and cons that you mentioned are too general.
Can you choose the most relevant 2-3 pros and cons for the microservices ?
Ok, I understand the issue for AWS Lambda, though one could schedule a job with Cloudwatch or similar to call a Lambda every 5 minutes, with 1M free calls per month this isn’t really a problem.
I mean yes you technically can, but I’d rather just use the service as it was intended. If I had long running services I wouldn’t put them on Lambda style provider.
The REPL driven development is really useful when it comes to microservices. That’s because since a lot of things rely on calling remote APIs often owned by other teams, being able to quickly experiment with them is really helpful in order to learn how they work and quickly be able to integrate with another microservice. Otherwise, you need to rely on documentation (which is often missing for internal microservices), or get the other teams involved, but they’re busy people too. For example, I might even try out failure cases to see what kind of errors are thrown when, and be sure to properly handle them. The REPL is also really handy to debug prod issues, having one in prod can save you a lot of debug time.
Clojure’s productivity focus. When you go the microservice route, you create a lot of extra ceremony. What would have just been a few functions now require a whole API layer running on its own infrastructure. It really helps to reclaim the added time that Clojure is so productive, and create small code bases.
Clojure’s runtime contracts. One thing is lost when you move to microservices, that’s full program static type checking. Your app is now split in many services where all your data is exchanged over an untyped protocol like HTTP. Thus making static type checkers useless. That’s where Clojure’s embrace of safety through dynamicity comes to the rescue. Using Spec or Schema to validate at runtime the data exchanged between your microservices is really handy and will save you a lot of trouble.
Performance. Splitting your app into microservices often comes with extra cost. It becomes hard to maximise servers resources usage when every little component gets its own fleet of servers. So, I find that to compensate this, I’d rather use something that can really utilise the cores and really make good use of single machines. In that respect Clojure will give you way more mileage then say Node, Python or Ruby. Now, Go, Java, Scala, C#, Rust et all will fare equally or better than Clojure in that regard, bur they trade away my other pros.
I can’t really think of any cons that affect microservices specifically to be honest. @Yogthos might have given you the best one already. If you wish to design some of your microservices with a serverless architechture, the slow boot times might make it more challenging. If you weren’t planning to go serverless though, I’m not sure there are any cons.
I recommend you read this short article: https://hackernoon.com/im-afraid-you-re-thinking-about-aws-lambda-cold-starts-all-wrong-7d907f278a4f
Bottom line, you incur a cold start for every concurrent request, not just the very first request. Warming up concurrent lambdas every 5min probably defeats some of the benefits of using them in the first place.
Language choice is an interesting point to discuss I think.
If you’re going to use Clojure for everything then you’ll end up with a lot of internal libraries (we tried having a big shared, kitchen sink lib and it didn’t go well)
In my experience (17 services and increasing , varying in size)
- you can tap into JVM, uberjars for deployment simplify certain things
- Clojure is very terse so most code is focused on business logic
- carving out services out of namespaces is super helpful
- startup time and memory usage are a thing you really need to consider, on the other hand - threads and concurrency mean that you will run fewer instances of given service
- you can use a lot of tools from Java ecosystem - client libraries, data serialization etc
- we settled on Component as a way of giving minimal structure to applications, before that there was a wild west of approaches and no real patterns, now each service has set of base components (nrepl, rabbitmq, pg, etc)
I talked about some (most?) of these topics at one ofrecent Clojure meetups in London: https://skillsmatter.com/skillscasts/12228-building-a-product-with-clojure-lessons-learned
Thanks for the tip! By the way, I guess that if you have that kind of workload a server might be much better than lambdas.
That makes lot of sense!
You mentioned the REPL as a pro for writing microservices in Clojure. But other dynamic languages like node python and ruby also offer a REPL: they call it a console.
What parts of the Clojure REPL makes it stand out comparing to other languages console?
There’s two advantages to the Clojure REPL over most others like the ones offered by Ruby and Python.
- The language is designed around it.
- The tooling is designed around it.
Everything (which could be implemented within the constraint of the JVM), is reifiable in Clojure. Think of it as everything is self aware, and can be dynamically swapped in and out. On top of that, the general way of programming in a functionally pure and immutable manner promoted by Clojure means your state is normally well isolated, so it’s easier to replace things without making a mess of the running state.
Now on top of that, the tooling embraces the REPL in that the REPL is actually a REPL server, and not a command line tool. It takes code through a remote API, and returns a result over the network. Middlewares can be added to pre process or post process code sent and returned by it. Other middlewares can extend it with custom features, like asking it for a list of all variables defined in an active namespace.
In turn, editors and IDEs provide user interfaces to the server REPL. This allows tight integration. So for example, an editor can give you a way to send all the content in a file buffer you’re editing to the REPL. Or send only selected text, or only the s-expression (the thing between two matching parenthesis) to be evaluated by the REPL, and in turn it can show you the result of it inline with the code inside the editor pane.
The REPL is used in this way to actually give editors all kinds of features, like show documentation, auto-complete, even refactoring.
Even command line console like Clojure REPLs are able to show doc, tell you the argument to a function, auto-complete code, etc.
Finally, because the standard REPL is server based, you can remotely connect to it, letting you hook in a REPL inside of an active process, potentially even running on another machine. This can be really useful for debugging. So think about it, you started an application, not within a command line, there’s no console, this is your microservice all bootstrapped, running, handling request, and now if you wanted to inspect its internals, you could just connect a REPL to it and poke around, and later disconnect, all without having to interupt it.
Edit: Here’s a cool small video demoing how a Clojure console REPL supports IDE like features: https://asciinema.org/a/160597 which shows you some of the power of Clojure REPLs.
Thank you @didibus for your detailed explanations
@Yehonathan_Sharvit not specific to microservices, but I tried to answer that here: https://vvvvalvalval.github.io/posts/what-makes-a-good-repl.html#what_makes_a_good_repl?
Stuart Halloway also provides a good comparison in this video: https://vimeo.com/223309989#t=350s