Every time this comes up I promise myself I'll write a full post, and then I try to write a full post and give up 3000 words later. Here's my usual, now-about-two-years-out-of-date-spiel:

-------

Alloy comes from the Z line of “systems are basically giant relational algebra problems”, while TLA+ comes from the LTL line of “systems are basically giant temporal logic problems”, which leads to a lot of fundamental differences between them. They also had different design philosophies: Jackson wanted a language that was easy to build tooling for, Lamport wanted a language that was easy to express complex logic in.

One consequence of this is that Alloy is a much smaller language: you can fit the entire syntax on a single sheet. It’s also much easier in TLA+ to write a spec that you can’t model-check, while with Alloy you have to be actively trying. It’s also impossible to write an unbound model in Alloy, so you’re guaranteed that every spec is over a finite state space. Which gets rid of the common TLA+ runtime problem of “is this taking a long time because it’s a big state space or because it’s an infinite state space”.

Re tooling: Alloy converts to SAT, which makes model checking much, much faster. Specs that would take minutes to check in TLA+ take less than a second in Alloy. Being able to model-find as well as model-check is really nice, as is the ability to generate multiple counter/examples. The visualizer is really useful, too, especially for showing counterexamples to nontech folk. But the thing I honestly miss most when working in TLA+ is the REPL. It’s not the best REPL, but it’s a hell of a lot better than no REPL.

Okay, so drawbacks: Alloy6 added a notion of "time". This expands the space that Alloy can do, but the syntax is really footgun-heavy. If you forget to update a variable in TLA+, you get an error, if you forget to update in Alloy, it'll just assign whatever value it wants to that variable. I asked one of the developers and apparently it's not something they can easily address in the current semantics. Other problems are it can only check up to a given time-bound, which makes complex properties like liveness hard to check. The more “timelike” the problem gets, the worse Alloy becomes at handling it.

Alloy also doesn't have composable collection types. You can model "a queue of messages", but you can't easily model "a queue of messages for each server". It's possible but you have to be clever, whereas in TLA+ it's straightforward.

Less fundamental but still a big problem I keep having: no good iteration or recursion. Transitive closure is pretty nice but it’s not powerful enough. For example:

sig node {edge: set Node}

Is N1 connected to N2? Easy, just do `N1 in N2^.edge`. How many nodes is the shortest path? That’s much harder.

Which one is more appropriate to use really depends on your problem. As a very, very, VERY rough rule of thumb: TLA+ is best you’re most interested in the dynamics of a system: the algorithms, the concurrency, etc. Alloy is best when you’re most interested in the statics of a system: the requirements, the data relationships, the ontology, etc. My standard demos for showcasing TLA+ are concurrent transfers and trading platforms, while my standard demos for Alloy are access policies and itemized billing. You can use each in the other’s niche, too (see Zave’s work on Chord), so it’s a strict division.