How do you approach long-term maintainability

Let me start off by giving a little background on myself just to set the context a bit …

I’ve been doing software development for almost 15+ years professionally. And probably 10+ years before that. A long time. I love building software and and learning languages. I’ve worked on applications ranging from “Enterprise” to “Startup.” And almost every application I’ve encountered is architected incredibly different (especially modern day “serverless” or “microservice” based patterns).

I come from a C# world. C# is a strongly typed language and I’ve used it for a long long time. I picked up Ruby as my first dynamic language and fell in love with it instantly. Obviously the two languages are incredibly different and the beauty of the Ruby language and the speed at which you can develop was an amazing change coming from the verbose world of C# (at the time it was like C# 3.0).

Eventually I found Node and Javascript and build some front-end and back-end tools using those as well. I’ve built many things using express, koa, react, vue, etc. I’ve leveraged Typescript and the benefits of the type system on top of Javascript. And I’ve explored Go as well.

I also discovered Erlang and Elixir along the way although I’ve never used them in a “professional” setting. Mostly just for personal projects. I love the benefits of functional programming. The Beam/OTP are absolutely amazing. And I think Phoenix and Ecto and some of the core utilities in the Elixir community are absolutely phenomenal. I absolutely love my personal projects that I’ve built with Elixir.

At this point, it feels like I’m humble bragging a bit and I don’t mean to come across like that. So let me move on to the point. I know this is highly subjective, but here we go …

Of all the languages and frameworks that I’ve seen and used … C# and Typescript (strongly-typed compiled languages) tend to provide the best long-term benefits of applications that I’ve developed. Especially applications that span 2+ years. Or personal projects that you start but then have to come back to months later and pick up again. Having a compiler and a type system just helps me forget about things for a while and then come back to quickly discover what it was I was doing.

The C# compiler and developer tools (Visual Studio, Rider, Resharper) have really spoiled me as a developer. The ability to refactor and debug code, in my personal opinion, is just unmatched by almost any ecosystem I’ve seen.

I have seen medium-to-large Ruby applications turn into monsters that are difficult to understand and it just feels like whack-a-mole when you try to change things in a production environment. I know the “fix” to this is to just “add massive amounts of tests.” And perhaps that is the answer? But in reality I’ve never experienced a well architected, well maintained, easily refactor-able application with dynamic-based languages (Ruby, Python, Javascript). The compiler just seems to provide you a safety net that is extremely beneficial (in my experience).

And since I haven’t seen an Elixir application in a professional setting. I’m curious if others could provide me any insight into their experience with maintaining a medium-to-large sized application in Elixir over the period of 1+ years of development.

Taking into account things like refactoring, debugging, tools, etc. If you could provide any insight on how not having a compiler or type system as a safety net has been in your real world experience. Or any other things that you have found beneficial about the medium-to-long-term maintenance of applications that you have built with Elixir.

Thank you so much in advance for any insight or thoughts that you can provide.

6 Likes

To me, that’s exactly the problem I want to avoid. I don’t want to be beholden to a particular tool or IDE.

As for strong typed vs dynamic typed, my opinion is that a strong typed language is best for larger team of people from very green to very experienced people, and a dynamic language is best for a small team of people that are more or less on par to each other in skill level. You can’t change your team, but you can change your languages to suit your team. Long term maintainability also boils down to the quality of your team, not so much to the tech stack.

1 Like

Short answer: Yes, tests, mostly, not a “massive amount” but a decent bunch of functional tests that ensure that the moving parts of the application behave as intended. Having a lot of typespecs and @spec above functions helps the refactoring process too.

Also, the language being functional, unit testing and refactoring is quite easy in the long term. It is very efficient to be able to call a handle_call function directly if needed compared to testing objects internals in OOP languages ; and to work with functions that are side-effects free.

I have not found working on projects I did not touch for 1 year particularly hard but ymmv.

Small introduction with my experience:

I was working couple of years on Ruby On Rails app. It was next generation of another old app. Both of them were Ruby. I’m not sure if first one used Ruby On Rails. I think now it’s more than 15 years old “system”. 50% used regular RoR html and another 50% used api with Angular. At that time it was about 50k LoC of Ruby and 80k of Angular (I think at that time it was called AngularJS 1.7). About 300-400 data models in db. So I think it was huge project. I think I don’t have to talk about pain with that kind of app. We had a lot of happy days, nights, overtimes… :smiley:

For last 4 years I’m working with Elixir. I was first dev(CTO) in company( I think second person at all). We created about 20 applications in Elixir. I’ve been teaching/helping to learn Elixir about 20 devs. But our main focus is for our product in online payment world (a lot of sensitive information, regulations,…). I’ve met Elixir developers which came from all around the world languages (C#, Java, JS, Ruby, PHP,…).

These are my short answers for your huge question:

  • @spec can give you more than you think and dialyzer will have your back
  • it’s a lot about readability ( syntax with |> will give you more readable code at the end of the day. I have rule which saved me a lot of days: 3 sec to understand what the line of code do, if not, it’s bug)
  • functional programming is not about magic as OP, it’s just about some functions. (I spent tremendous amount of time to figure out how some object with 20 layers work when I was working with Ruby. With elixir, you have to just found function or some stray macro, that’s it).
  • it’s more about design of your code than about the language.
  • It’s very easy to make/generate documentation (direct link from doc to specific line of code, …)
  • Recent upgrades in Elixir compiler give nice tool for some kind of “validation”
  • Erlang is here for more than 30 years and Elixir is build on it. (not exactly, right…)
  • Developer happiness

I’m pretty sure I forgot a lot and some is OT. But at the end: Language can be perfect for maintenance but when you make dirty app, it will be hell even after 1 week after you create the code and not for couple of years.

4 Likes

Specifically addressing the matter of types:

My background: CS degree including various languages, then about 36 years of experience, at first mainly C, then mainly Ruby, with lots of other stuff mixed in, learning Elixir for the past few years, haven’t had a chance to use it for work yet (but would love to and am available for very-part-time remote work, pardon the plug).

Before coming to Ruby, the vast majority of large work I had done (as opposed to quick little scripts) had been in C, or other typed languages like C++, Java, Pascal, FORTRAN, [Visual] BASIC, and so on. When I did things in scripting languages like csh, bash, Python, Perl (typed in a sense by requiring type-declaring warts on front, but not enforcing variable pre-existence), and so on, the lack of types sometimes tripped me up, as I forgot what types some function took or returned.

By the time I came to Ruby, I had come to realize the value of tests. That was in fact one of the things that attracted me, that they actually gave half a damn about quality (unlike most people I had worked with in most languages), and tried hard to attain that through thorough unit testing.

I thought that the lack of types would trip me up. But hallelujah, they showed me how to do testing much better than I had done before, and in ways that worked well with “duck” typing. I got comfortable with that, and now realize how much more vital tests are when you don’t have types… but how you can have excellently maintainable software, without types.

That said, though, I have not yet tried the other extreme, languages with such a strong type system that if it compiles it is almost certainly correct. I’m a bit skeptical of such claims, but would like to try it . . . one of these years. :slight_smile:

3 Likes

Strong and static type system helps a lot with that, agreed. Still, I’ve seen uncommented Rust code that’s very hard to decipher (even with type annotations) so comments around the code in these cases can help avoiding wasting your colleague’s day.

First priority: code must be readable. You must be able to decipher it while being half-asleep. If I have to write comments in my code often, then I feel I have failed as a writer.

Second priority: it must be terse. But if it gets so terse that it can’t be deciphered then it should be changed to something slightly bigger that’s more readable.

Sadly Rust in particular is quite verbose, at least compared to Elixir. All the more reasons to have nice and tidy functions that do only one thing and do it well.

I get where you’re coming from but this gradually stopped being important for me throughout the years. F.ex. refactorings done manually is a bit slower, yeah, but also helps you avoid mistakes that the tooling-led refactoring will not notice.

Using only Emacs/VIM, a language server, and a shell is an acquired taste and it’s debatable if that’s a better or worse setup than a fully integrated IDE.

I am definitely not the part of the most experienced in-production-Elixir-devs around here but I used it in at least 7 separate commercial projects and my observations are:

  • Elixir and its runtime (the BEAM) are awesome in many ways but you need a lot of tests, especially if your team doesn’t practice strict and exhaustive pattern-matching and function guards. Dynamic language projects need more tests to gain the same sense of security compared to strongly and statically typed languages.
  • At one point agreeing on a file/directory structure for a project becomes extremely important because people want to be able to intuitively find where they have to work in order to complete their ticket. So Elixir’s freedom in this regard is a double-edged sword. Personally I’d prefer a stricter approach like Rust’s (and I think Java’s).
  • It’s OK to abstract away things even if they are painfully obvious at the time. For one personal project I always used my own configuration provider and thus never had to worry much about Elixir’s changing stance on compile-time / boot-time / runtime configuration. I just made several modules where the lifecycle of the config options was explicit (like injecting module attributes and/or functions at compile-time with macros sends the message that this config is only for compile-time and can’t be changed at runtime; another is fetching config options from an etcd-like program, which should be strictly a runtime configuration). TL;DR: Configuration in Elixir is a moving target, it’s OK to abstract it away.
  • It’s recommended to contribute to open-source efforts to bring extra features to the ecosystem, assuming they are already underway. Case in point: telemetry. Absolutely don’t roll your own. The community is small and generally cooperative. People in these communities love PRs.

There are no two ways about it: when my Rust programs compile, most of the bugs I could have introduced unwillingly are already eliminated. Hence Elixir does require stronger test discipline.

Something I had previous colleagues fight me over: I prefer to utilize as strict a strong typing – even if runtime and not compile-time – as possible. To that end I write @spec-s and function guards, a lot. I recognize this is rarely the business priority but I prefer to invest in my and my team’s future sanity.

It can be overdone, however, and I have crossed the line before. Sometimes it’s OK to just leave a function like this: find_applicable_config(configs, criteria) with zero annotations because when you’re iterating the shape of these pieces of data can change many times.


TL;DR: Write tests. A lot of them. Don’t be afraid to sometimes argue with the business people. They’ll always want the cheaper and fastest to develop program that’s also flawless. Part of our job is to educate them about the tradeoffs. I had success with a vice-president once by telling her that she wouldn’t feel safe in her car if it didn’t pass suspension strength tests and that more tests and stricter typing in projects translates to almost the same thing.

5 Likes