Hacker News new | past | comments | ask | show | jobs | submit login

I don't see much discussion of event-sourcing simply using a SQL database (i.e. skipping the CQRS part). This would allow you to keep your CP (strongly-consistent) semantics.

While this clearly wouldn't work in high-volume cases (i.e. where you _actually_ need CQRS), it seems like this would be the simplest option for many systems. I see a lot of articles advocating for immediately jumping into CQRS, which seems like a big increase in architectural complexity.

Does anyone have opinions/experience on this approach?




I went with this approach on a recent project, for two reasons: * Tracing/history/auditing * Structuring the service around these events, it became trivial to add new "event types", rather than expanding some big hairy PATCH endpoint or similar. In other words, when I needed a user entity to be able to belong to a group, I just added a new event type, "user/join-group". Having a single endpoint for all events had two other nice benefits: batching became trivial, as well as doing several events transactionally (all events in one requests are processed in one transaction).

It's been running in production for a couple of months now, and it's been working great! The main drawback I can think of was that it obviously didn't work well with Swagger out-of-the-box, had to do some custom handling for showing all the available event types.


Don't you still need a queue in your example? do you lock the whole table when you insert a new record to the table? can you elaborate how this solves consistency?


I don't think you need a separate queue; if you have an "Events" table then you can just write everything there.

It solves the consistency problem because you can create your event inside a transaction, which will rollback if another event touching the same source is created simultaneously.

E.g. if you have these incompatible events in a ledger:

CreditAccount(account_id=123, amount=100)

DebitAccount(account_id=123, amount=60)

DebitAccount(account_id=123, amount=60)

You'd want one of the debit transactions to fail, assuming you want to preserve the invariant that the account's balance is always positive. You could put the `account_id` UUID as an `Event.source` field, which would allow you to lock the table for rows matching that UUID.


If your idea in the example is that the second "debit" is created by another transaction while your transaction is in progress, then this will not work out. Firstly it requires a dirty read, which is nothing I would rely on in a transaction. Secondly, if the dirty read works, assuming the outcome of several rows is just a read operation, which forces you to rollback on the client and still leaves a window for inconsistencies if you decide to commit. Maybe SELECT .. FOR UPDATE can do a trick here, but that is like giving up life.

To round this up: RDBMS are bad for queue like read semantics. All you can do is polling. Which is even worse if you end up being lock heavy.


No matter how you model things and no matter what technology you use, a race condition like this needs to be handled one way or another. You either handle it such that the data is always consistent, or you handle inconsistent data.

You can use an SQL database with transaction and locking to ensure that you will never debit money that isn't there. Or you can save commands in a queue that only a single process at any given time (that incidentally includes the SQL scenario). Or you can use a distributed consensus algorithm with multiple stage commits. There is no way around it.


I’ve implemented this.

TLDR: orm’s + large transactions resulted in a lot of unexpected complexity.

At some point you get really big transactions because one write triggers five process managers which all trigger more writes and so on and so on. Performance was not a problem but I was surprised by the complexity of these big transactions in combination with an orm.

I dont have a concrete example but over the course of two years we have encountered multiple bugs that took days to solve. Theres one of these bugs that we fixed without identifying the root cause until this day.


Thanks for the perspective -- given what you know now, would you have built it differently if you could do it over again? Or was it the right call for the stage of your system, even if there were pain points?




Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: