Fork me on GitHub

(s/invalid? (s/conform :todo/task-event event)) -- it's probably clearer to check (s/valid? :todo/task-event event) (and switch the if/else clauses). You normally only use s/conform if you actually want to conform the data (i.e., get back a data structure labeled with how it matches the spec).


Look at multimethods as a more idiomatic way to write apply-event -- since it is essentially polymorphic on :event/name


(when (s/valid? (s/keys :req [:event/name]) event) -- I'm curious why this isn't checking against :todo/task-event like the apply-event function?


ah good catch! i wanted to leave the event store open to also process other types of events that might be added in the future (there's no requirement on :task/uri or :task/title ) maybe i could add a smaller spec which i then compose into :todo/task-event and reuse there for the check


That sounds good. Having an anonymous spec just for s/valid? seemed a bit odd.


Overall, nice and clean and easy to read. Good job!


the formatting on apply-event is confusing to me. tasks is on the same line as the result of "task-completed" even though it's an else statement. I prefer a format like:

(defn apply-event [tasks event]
  (if (s/invalid? (s/conform :todo/task-event event))
    (case (:event/name event)
      (->> (select-keys event [:task/uri :task/title])
           (conj (remove #(= (:task/uri event) (:task/uri %)) tasks)))
      (remove #(= (:task/uri %) (:task/uri event)) tasks)

      ;; else


the formatting on apply-event is confusing to me. tasks is on the same line as the result of "task-completed" even though it's an else statement. I prefer a format like:

(defn apply-event [tasks event]
  (if (s/invalid? (s/conform :todo/task-event event))
    (case (:event/name event)
      (->> (select-keys event [:task/uri :task/title])
           (conj (remove #(= (:task/uri event) (:task/uri %)) tasks)))
      (remove #(= (:task/uri %) (:task/uri event)) tasks)

      ;; else


that is a lot more readable, yeah on the if its not all that clear what's happening sometimes, adding comments is a good idea. thanks for the tip


apply-event combines validation and the event processing. my intuition is that those should be separate


currently, apply-event will continue to grow in size if you keep adding more events. check out for an alternate approach


right - in other languages I would sometimes start a function with some guards that exit early (like using :pre i guess? - but i didn't want to throw an error) that was my intention there


i'll have a think where i could put it


what's the reluctance to throwing an error?


i'm not sure if its an exceptional (unexpected) thing, it could happen that the event store contains some old events or stale events, or events with an old schema that shouldn't be reduced


i prefer to reserve exceptions for things that should stop the application


you could apply-events return some error-value rather than throwing an exception


the issue with ignoring invalid events is that ignoring events is unlikely to be the right course of action


it means there's probably an issue that needs to be addressed and silencing the problem just makes it harder to track down


makes sense, at the very least a warning should be logged somewhere and maybe someone gets an alert


i was thinking of leaving the event store open, so another producer could add a new type of event at any point


but that would break my application then if i returned an error


for a more rigorous approach, I recommend very highly


my point is that your application is already broken


ignoring the error doesn't fix it


haha 🙂 thats a fair point, added it to the reading list thanks


I was working on a mobile game where you run a virtual restaurant. if the game received a recipe it didn't recognize, rather than crashing , it just used the first recipe in the list of recipes. one day, we pushed an incorrect config and all our player's dishes turned into veggie burritos


i have to admit i'm not sure what the best practice is here in the event sourcing case, i'll need to look into it


You can always reify exceptions and treat them as events, by which I mean you can catch them, call Throwable->map on them, and transform them into events -- if you want.


ooh thats a nice idea


I have quite a bit of code that does something similar to (try body (catch Throwable t {:exception (Throwable->map t)}))


Then you can pass it around like data and still tell exceptions from "real" data.


Just to expand on this, exceptions are the only error handling mechanism clojure has. Also nil punning for some cases. In clojure, exceptions are not reserved for shutdown/panic situations


Oh, good catch! I missed that completely.


not sure whether it's good practice or not, but I often have comments for else statements in conds and cases


additionally, I don't think apply-event should return the tasks unmodified if an unrecognized event is sent. I would probably either remove the else statement, throw an exception, or return some sort of error


My (deleted) comment about adding a docstring still stands (I think everything should have a docstring really, including ns) -- but in task-event-handler I'd name the argument send-fn to avoid shadowing the built-in send (in clojure.core) which confused me when I read the body.


if I send an event to an API and I send it an invalid or unrecognized event, I would hope to receive an error rather than it silently do nothing.


^ although adding error handling throughout the app is whole 'nother layer of complexity which is probably a learning exercise in its own right 🙂


right. the simplest error handling implementation would be to simply remove the case's else statement and let the generic error handling take over


it's not the best error handling, but at least it would indicate something went wrong to the API's user


good point! you're right if the application reducer encounters an error it could lead to an incorrect state being created from the events - i'll have a think about that


thanks for the pointers! really appreciate the help