As part of an exercise to learn how to use Polylith and see how it would work with multiple systems deploying to different architectures, I made a small example app (a telegram bot which parses messages and repeats back what you asked for).
You can find the source here: https://github.com/miridius/telegram-bot-polylith-example
And you can see the bot running here (try saying "more things please"): https://t.me/bbg_harold_dev_bot
Unfortunately I can't share the original repo (including commit history etc.) as it's part of a larger repo with some private code, so I copied all the code that I can share into a new empty repo.
- 2 systems, one deploying the whole bot to an AWS JVM Lambda and another for deploying to an AWS Node Lambda.
- Each system has a base which only has the bare minimum of code to handle the platform-specific lambda input and output conversions, everything else is in components
- All components are written as .cljc files, and shared by both systems
- All tests are run twice: once on the JVM and once on Node using doo. For components, the tests are .cljc files, for the bases, each base has its own test file (as .clj or .cljs) but the tests themselves are mirrored.
lein test is aliased to run both sets of tests.
- Each system is configured to use serverless.io to deploy to AWS and set up API gateway endpoint
- Automatic compile/test/build/deploy is set up using GitLab CI.
- Code comments and documentation. Since this is was just a personal experiment, I didn't write any docs. But after getting this far it occurred to me that it might be worth sharing, so please forgive the lack of documentation!
- Since caching on GitLab CI is kind of a pain to set up, I just build from 0 on every run. To be honest I'm not really sure how restoring cache and then doing a partial build is meant to work. Are the previously compiled class files restored? Or does it just affect which tests are run, and only changed systems are built?
- The next thing I'm working on is a Web UI, to see how it would work to deploy static web assets and/or a web server as one or more polylith systems from the same project
- The two lambda handler bases are all custom code, what I think would be better for long term maintenance/testability/reusability is to write (or use an existing) AWS lambda <-> ring adapter and then write the app as a regular ring handler (in a separate component) which the base then simply converts to a lambda handler.
If you actually want to use this code rather than just looking at it, the steps are pretty simple:
1. Install serverless
lein polylith build
serverless deploy from either the
systems/harold-bot-node or the
systems/harold-bot-jvm directory. Take note of the URL for the POST endpoint created (it should end in /update)
4. Create a telegram bot, and use the telegram API to set your bot's webhook to that URL (ideally this would be a step as part of CI)
CLJ vs CLJS:
In case anyone is wondering, I did some not very un-scientific E2E performance tests of the same bot running on JVM vs Node lambdas, and it was about as expected: the cold start response time is much worse for the JVM lambda (about 7,000ms vs ~1,000ms) but the executions after that are a tiny bit faster (~50-200ms vs ~100-250ms). So for this use case I would definitely go with Node, but I think in many other cases JVM is better. It's really nice to know though that you can build all of your components completely independently of which platform they will run on, and the magic of Polylith lets you decide later which system you want to use 🙂
Comments and Observations:
I learned a lot about how Polylith works from this experience, and my overall impression is that I absolutely love it! It made it so much easier to reason about components and bases as being separate things from systems and also having the development environment as a separate abstraction as well made so much sense. I built the JVM system first and then added the Node system afterwards, and the Polylith structure made it so much easier than it would have been otherwise. All I had to do was rewrite the same base (30 lines of code) in clojurescript and then rename all my component's src/test files to .cljc and it all just worked 🙂 It has been more than enough to convince me to start using Polylith in my real job for my current project, and not just my own fun projects.
They polylith tool saves a ton of time on doing things manually, and is definitely a godsend! I did feel sometimes that it is still somewhat in its infancy however as there are a lot of little issues that could be polished out (which I've written down and will make GitHub issues for when I have a chance) as well as a few ideas coming to mind for added features, such as making a deploy command, similar to build (and also depending on build) but with a deploy.sh script instead.
One thing that I did find a little bit awkward with the polylith process (but I'll probably get used to dealing with) is that I often wanted to edit files which were not visible inside the development environment because they were not any of the standard sym-linked files/directories. For example:
- workspace .gitignore
- workspace CI config file
- serverless config files inside each system
What I generally did is just manually added some new symlinks to those files to somewhere inside the dev environment that they were the best fit, such as the project files directory.
Summary: Polylith even just as a concept is amazing, I 100% think this is the future of systems architecture. I will use it again for sure and have already told all my friends!