
Two years of Elixir at The Outline - davydog187
https://blog.usejournal.com/two-years-of-elixir-at-the-outline-ad671a56c9ce
======
latch
I'm bullish on Elixir and I agree that ExUnit deserves to be singled out.

Whether you consider Elixir fast or slow probably depends on what you're
coming from and what you're doing. Personally, I wish the performance story
would significantly improve.

But, the reason I think Elixir should be your goto language comes down to
process isolation and the ecosystem around it (the synergy of the runtime,
libraries and language). If you're doing a pure web app, you might not fully
leverage it. But, for anything that involves long-lived in-process data (like
a workflow/pipeline, or persistent connections), the way to approach this in
Elixir (with supervisors, processes, message passing and pattern matching)
tends to result in highly cohesive and maintainable code. Message passing is a
pretty effective antidote to coupling.

I just finished writing code that had to take a stream of unordered time-based
data and dedupe it over a small window. I'm not overly pleased with the final
result, but it's completely isolated from the much larger app it sits in, easy
to refactor/rewrite, with no risk of someone taking a shortcut and just
accessing some internal detail, and testable.

I feel like I wrote a microservice, but without any of the challenges of a
separate app.

~~~
ilovecaching
So, just to speak about the BEAM, which is the part of Elixir I am familiar
with, I don't think any BEAM language will ever be a good _goto_ language. The
BEAM isn't good at general purpose computing. It's not particularly efficient
at performing calculations or using memory. It's good at reliably performing
as you increase memory pressure on your system (via message passing). You can
scale a business to a decent size on a smaller number of fatter nodes (high
RAM machines). Connections will keep pouring in and you'll get a consistent
ms.

That said, you'll run into issues once you hit a certain level of scale. I've
had to make patches to the BEAM just to keep us scaling, and I've never seen
someone get away with stock dist. You pretty much have to rewrite pieces of
OTP.

Now is Elixir ok for a CRUD app serving 10G of traffic? Of course... but this
is also the age in which every language has a solution to the C10K(, C100K,
and more in many cases) problem, and there are compelling reasons to write
your CRUD app in Go and get better library support, static types up front
(trust me, you want a static type system), and lower latency.

Personally, I'm bullish about Rust. With async/await landing, it stands a good
chance of providing the same focus on safety and reliability while being
infinity more flexible at what it can effectively. It's a better _general
purpose language_. Also, the actor model (and CSP as well) aren't the end all
be all of concurrency patterns. Rust only provides the building block
(futures) so it's much easier to choose the concurrent pattern that fits best
for your problem. You can sting a future using the actor model, so it's still
an option, but you aren't limited to it.

~~~
emerongi
Ok... yet 99% of development is trying to write bug-free and readable code and
do it pragmatically.

Elixir gives you that plus a lot of goodies that are really hard to get in
other languages. The creators of Erlang were really clever and in my opinion,
so are the creators of Elixir. If I ever were "bullish" on a language, Elixir
would be it.

Elixir is a very pragmatic language. It doesn't get in your way, but still
forces decent habits and design on your application.

~~~
ilovecaching
> bug-free and readable code and do it pragmatically

Erlang has historically been a bad choice for this, and there's been a lot of
effort (dialyzer, records, maps etc) to make it better.

Dynamically typed languages are, in my experience, not worth the cost of
maintenance and readability. In Elixir and Erlang it's possible to create
function signatures that tell you almost nothing about the shape of the data
you expect to pass in. This makes it really hard to collaborate at scale.
Various attempts to enforce dialyzer use are usually met with 'we need to move
faster' or 'we'll add them later'.

So if you want to write bug-free readable code, I think Rust is a much better
choice. It's statically typed, and the addition of traits and lifetimes tell
you _so much_ about how a function or type will behave, and there's no need to
sync specs or docs. The code doesn't lie.

~~~
StreamBright
Exactly, this is why the most reliable piece of mission critical system was
written in Erlang.

------
rdtsc
> 90ms is a 90th percentile response time on The Outline. Our post page route
> is even faster! We got this performance out of the box

That's one of the features you get when using BEAM VM -- your code will likely
be scalable out of the box as it's already built out of isolated independent
small processes. If you want to handle more requests just run on a bigger
machine.

> The Community is wonderful!

I use Erlang mostly but one major thing I admire about Elixir is it's
community. Jose and team did a great job there. So it really has both - the
great technical stuff, built on BEAM VM which was battle tested for decades,
and the great community and tooling.

> As soon as you get data from the external world, cast it into a well known
> shape.

Good advice. Have sane validation at the edges and then convert data to some
internal format that's easy to handle. As opposed to defensively sprinkling
validation conditionals everywhere in the core code. That's makes the code
cleaner and easier to reason about.

------
ilovecaching
As a long time Erlang developer I was suprised when I first heard of Elixir,
and even more surprised at how nice it was.

That said I think Elixir is very unerlangish. It’s very much Ruby ported to a
new platform, it’s heavy on meta programming with macros and using a giant
solve all your problems web framework (Phoenix). Compared to Ruby on Rails I
think it makes a lot of good arguments. It’s faster, mix is a better package
manager, and the BEAM is reliable at small to medium scale.

Of course I recently posted about how I was moving away from the BEAM to Rust.
I think Elixir has reached peak adoption, and I think the days of large
frameworks like Rails and Phoenix that ask you to program in a DSL are
sunsetting in favor of client side frameworks and smaller web services built
off of thinks like Go’s net/http.

I also think the reliability of BEAM is happening at the level of
orchestration with containers and serverless. The BEAM itself doesn’t fit well
into the cattle model of reliability and it has scalability issues when you
reach a certain cluster size.

Erlang is also quite slow at processing data and performing computations. What
it does provide is reliable latency and load as you scale, which lets you grow
your build mess quickly without buying more hardware or performing software
miracles.

I’m more optimistic about Rust because it provides the reliability I’ve come
to expect from Erlang. We’ve been able to scale Rust to handle the same load
as our Erlang cluster with a more manageable code base, lower average client
latency, and lower memory usage on the same number of machines.

I also know that people like to treat dynamic vs static typing as a matter of
taste, but in my experience dynamic typing only works until you reach a
certain level of scale. I’ve watched several python and erlang codebases turn
into incompressible messes because of this. This is why we’re seeing things
like typescript and mypy become requirements rather than nice to have. So I’d
rather just bite the bullet and get a really nice static type system up front.

~~~
mrdoops
> "I think Elixir has reached peak adoption"

I'd say the opposite. Core libraries have reached stability and engineering
hours are switching to making easy what classic imperative/OOP paradigms have
trouble doing:

\- Dynamic web development without giant Javascript dependencies(Liveview,
Drab, etc)

\- Scalable distributed systems without giant teams (Firenest, Phoenix
Channels/PubSub)

\- Concurrency and data-infrastructure (Flow, Genstage, OTP)

\- Reactive event-driven systems: OTP and the Actor model makes event-sourcing
easier than you'll see anywhere else.

\- Usual conveniences: User-management, HTTP clients/libraries

The next 2 years are going to get wild for Elixir.

~~~
hassan_shaikley
For sure, and sockets are first class citizens and testing is at the forefront
for all these things. Elixir is going to kill it.

And there's a lot of potential there for game backends as sort of a neat
bonus. Just saying. : )

------
tschellenbach
When working on V2 of our architecture we tried out both Elixir and Go
extensively. For our workload Go was roughly 8 times faster than Elixir, while
both handling concurrent workloads exceptionally well. (bit of background on
the type of work we do: [https://stackshare.io/stream/stream-and-go-news-
feeds-for-ov...](https://stackshare.io/stream/stream-and-go-news-feeds-for-
over-300-million-end-users))

That being said, I'd love to find an excuse to work on a little project with
Elixir and Phoenix. Some really cool concepts. Especially the build in support
for realtime is really nifty. With Go that's much more work to get up and
running.

~~~
Thaxll
> With Go that's much more work to get up and running.

Can you expand on that? Go is very fast to get up and running, you DL go
runtime, git clone a repo, go build . and have your single executable ready to
go.

~~~
tschellenbach
This bit of Phoenix: [https://phoenixframework.org/blog/the-road-to-2-million-
webs...](https://phoenixframework.org/blog/the-road-to-2-million-websocket-
connections) Takes substantially more work in Go.

~~~
Thaxll
Thanks for the answer, for Go it doesn't take much most likely easier than in
Erlang: [http://goroutines.com/10m](http://goroutines.com/10m)

I never had to work on that many connections per instance, but knowing how
much faster Go is vs Erlang I woudn't see this as an issue?

------
momania
> consider going old school and rendering your HTML on the server; you might
> be delighted

One idea I'd wish more sites would consider.

------
catacombs
Yeah, writing about using Elixir at The Outline is cool and all, but let's not
forget this place wiped out most of its staff, including developers.

~~~
yawaramin
Care to point us to the story?

------
blissofbeing
I would like to use Elixir more at work, I'm hoping that we'll be able to use
it for some new projects. I love the language and the community.

------
pg_bot
Can you print an example of the random test error? I run a decent sized
phoenix project (150K lines) and likely can help you out if you give a bit
more detail.

With regard to associations, for simple belongs_to associations I will usually
write separate changeset functions where the first argument is pattern matched
against the record. I will give an example with Post/Comment

    
    
        defmodule MyApp.Blog.Comment do
          def changeset(%Post{} = record, params) do
            record
            |> build_assoc(:comments)
            |> changeset(params)
          end
    
          def changeset(%Comment{} = record, params) do
            record
            |> cast(params, [:body])
            |> validate_required([:body])
            # Continue with validations, constraints, etc.
          end
        end
    

In this case the changeset function calls itself once it handles building the
association. For more complex conditions, I will usually write a "Form" module
that may handle that case usually with a Enum.reduce combining multiple calls
to `put_assoc` so that I can make nested associations in one insert.

Hope this helps a bit.

~~~
enraged_camel
>>def changeset(%Comment = record, params) do

Small typo: should be _%Comment{}_

~~~
pg_bot
thanks, fixed

------
joevandyk
Does anyone miss or want type checking when working with Elixir? After working
with Ruby to build large systems for so long, I'm missing a little bit of the
safety that types provide. Typescript has been great.

~~~
yawaramin
Elixir has a specific kind of typechecking: success typing. You can set up the
Erlang Dialyzer tool to work with VSCode using the ElixirLS extension. It will
warn you about type errors. Success typing means that there is provably a type
error and you actually need to fix it, as opposed to the more common form of
typechecking wherein the compiler forces you to prove that there _isn 't_ a
type error.

True to form, when I see a Dialyzer warning and think about it for a bit, I
find that I _always_ need to fix the code. The error messages do take some
getting used to, though.

~~~
mercer
Isn't Dialyzer's success typing similar to TypeScript in that it's gradual
typing? Or am I muddling up my concepts?

~~~
yawaramin
It's similar and they are both considered gradual typing, yes. Dialyzer is
'better' in some ways though. For example, consider this TypeScript:

    
    
        function f(a) { console.log(a.x); }
        f({});
    

With the strictest checks turned on ( try it at
[https://www.typescriptlang.org/play/#src=function%20f(a)%20%...](https://www.typescriptlang.org/play/#src=function%20f\(a\)%20%7B%0D%0A%20%20%20%20console.log\(a.x\)%3B%0D%0A%7D%0D%0A%0D%0Af\(%7Bx%3A%201%7D\)%3B%0D%0A)
), TypeScript will give you this message: `Parameter 'a' implicitly has an
'any' type.`

Now consider the equivalent Elixir:

    
    
        def test2(), do: f(%{})
        def f(a), do: IO.inspect(a.x)
    

Dialyzer will give you this message: `Function test2/0 has no local return ...
The call 'YourModule':f(#{}) will never return since it differs in the 1st
argument from the success typing arguments: (atom() | #{'x':=_, _=>_})`

This is why I said the Dialyzer message takes some getting used to :-) But, it
just means that `f` needs to be called with either an atom (because you could
pass in a module to the function, and modules are modelled as atoms), or with
a map object with the `x` key and possibly some other keys.

------
davidw
> 90ms is a 90th percentile response time on The Outline

How much of that is the DB, and what DB are you using?

I like the BEAM environment quite a bit, myself, and have used it successfully
in several semi-embedded environments, where it really shines because of its
robustness and reliability.

~~~
davydog187
Postgres.

Currently, our mean response time for the route with the most throughput is
16ms. Its to measure exactly how much of that time is being spent in the
database, but the main query takes about ~6ms.

------
conradfr
\- Ecto is still a climbing hill for me. In fact I'm currently trying to warp
my head around changesets and associations.

\- Same for GenStage. I wanted to use them for workers pulling from an
episodic queue but the docs with the emphasis on consumers pulling multiple
events at once from a never ending stream didn't really speak to me. Tailoring
to my usage looked verbose: storing demand etc. I'll have to revisit when I'm
better at Elixir.

\- I still think caching should be part of Ecto in some way, at least in the
docs.

\- I would still like more straightforward template inheritance in Phoenix
(think Jinja2, Twig).

Excited for the future releases of Phoenix 1.4 and Ecto 3.

~~~
BenMorganIO
Changesets are great. I was hesitant when I first used them coming from Rails,
but they're very useful. Combined with the contexts, they help remove the need
for implementing ActiveRecord callbacks; which caused a lot of problems with
Rails apps as they aged.

------
helpme3
You should definitely CDN your data. A majority of a client-side latency is
spent round-tripping to the server. Using a CDN greatly reduces this time.

I don't know your workload, but 60ms doesn't seem that fast. Don't get me
wrong, it's pretty good, but for a read-heavy load it seems like this could be
optimized to be sub-10 ms at the 99th percentile.

~~~
pdimitar
An app I maintained that was doing anywhere from 3 to 50 DB queries per
request, 98% of the time was spent in the DB roundtrips. The Elixir code
itself took 0.2ms to 3ms.

It's _really_ lightweight. Can't be anywhere near Go or Rust of course but
it's provably much faster than Ruby, Python and PHP, and very often
outperforms JS on V8.

Most of the overhead I observed in Elixir apps stems either from external
network requests (DBs, K/V stores etc.) or CDNs / static assets. Most of the
Elixir code is almost invisible in terms of footprint.

~~~
jashmatthews
> it's provably much faster than Ruby, Python, PHP

This is simply not true. Ruby + Sinatra/Roda/Hanami + Sequel even performs
better than Elixir + Phoenix + Ecto in some cases. It's basically dead-even.
For example:
[https://www.techempower.com/benchmarks/#section=data-r16&hw=...](https://www.techempower.com/benchmarks/#section=data-r16&hw=ph&test=query&l=hos0z3&f=3y8-0-0-2zgg-0-0-0-2)

BEAM has the benefit of being a register-based VM vs the stack based VM of
CRuby but the immutable semantics of Erlang/Elixir negate a lot of the VM and
GC advantages.

~~~
pdimitar
You should read a bit more on how the various popular benchmarks manage their
different code bases and not just quickly believe the graphs. There's a lot of
story behind them.

Several languages, Elixir included, are misrepresented due to badly written
and non-idiomatic code (caveat: this was true a while ago, I haven't checked
lately though). And when its users try and open a PR they are shot down with
"We don't allow cheats". No cheats though, just very normal language best
practices.

So I disagree with you here. I maintained a ton of Rails and several Sinatra
apps and to say they are on the level of Elixir makes me wonder what kinds of
hosting budgets we both had access to and were they really so hugely
different.

(Sinatra was actually very decent but it being Ruby it doesn't scale well and
Erlang/Elixir work much more stably until they start having problems compared
to it.)

Again, don't just trust the popular benchmarking suites. You will quickly
discover they usually either have an agenda (like the Crystal language
creators always showing how quicker it is compared to Elixir, and refuses any
Elixir PRs -- seriously no idea what the Elixir community ever did to them) or
only pay lip service to representing all languages fairly (like TechEmpower
does).

 _(Edited because I wrote the first version after a nap and it came off
sharper than I intended.)_

~~~
bhauer
There is an implication here that we (TechEmpower) have rejected PRs that
would make the code in our Elixir tests more idiomatic. And if I understand
you correctly, when we did so, we explained that the implementation did not
comply with the spirit of our tests. To be clear, we _do_ reject pull requests
that we believe are cheating (e.g., caching results or other means to avoid
specific workloads called out in the test requirements).

Can you point to which Elixir-related PR [1] we rejected on that basis?

Some of our most recent Elixir commits were contributed by a gentleman named
Michał Muskała. For example, here are some he characterized as "minor
optimizations" [2]. Since he is a one of 16 people on the Elixir language
organization page [3], I take his contributions as representative of idiomatic
code or at least what the organization wants to represent as idiomatic. Do you
have a reason to disagree?

[1]
[https://github.com/TechEmpower/FrameworkBenchmarks/search?q=...](https://github.com/TechEmpower/FrameworkBenchmarks/search?q=elixir&type=Issues)

[2]
[https://github.com/TechEmpower/FrameworkBenchmarks/pull/3848](https://github.com/TechEmpower/FrameworkBenchmarks/pull/3848)

[3] [https://github.com/orgs/elixir-
lang/people](https://github.com/orgs/elixir-lang/people)

~~~
pdimitar
Thanks for chiming in.

Michał Muskała is a core contributor and his opinion weighs a lot so I am
really glad you guys are working with him.

I did check out the PR and his changes seem to be on par with what I've seen
in other repositories used in TechEmpower's benchmark. Not cheating, just
making sure not to use extra baggage that can severely hamper a framework's
performance.

As for rejected PRs, I looked but couldn't find what I roughly had at the back
of my head. It was a while ago and since I can't find it now I will have to
concede that I remember incorrectly. (Ouch.)

As I pointed out, my info was outdated. I see things have improved in the
meantime, which is good news!

Thank you for being responsive and transparent. Much appreciated.

