Ask HN: Has anyone fully embraced an event-driven architecture?

284 points by sideway ↗ HN
After reading quite a few books and blog posts on event-driven architectures and comparing the suggested patterns with what I've seen myself in action, I keep wondering:

Is there any company out there that has fully embraced this type of architecture when it comes to microservice communication, handling breaking schema changes or failures in an elegant way, and keeping engineers and other data consumers happy enough?

Every event-driven architectural pattern I've read about can quite easily fall apart and I have yet to find satisfying answers on what to do when things go south. As a trivial example, everybody talks about dead-letter queues but nobody really explains how to handle messages that end up in one.

Is there any non-sales community of professionals discussing this topic?

Any help would be much appreciated.

173 comments

[ 4.7 ms ] story [ 234 ms ] thread
Not sure if there are any communities. My general advice is to invest as much as possible in a good logging solution, traceability, and just general things to make debugging easier. Come up with a way to replay events easily. You'll thank yourself everyday a bug or issue pops up.
This 100x. Tracing, replay, etc are all invaluable even in non event driven systems
I absolutely agree with you, we are currently implementing a fully serverless and event-driven infrastructure and thinking about logging, traceability and debugging across 200+ lambda functions has been quite a pain. This is even more important as we manage financial flows.

We have written an article about how we try to fix that, if you are considering such an infrastructure on AWS feel free to read it:

https://medium.com/ekonoo-tech-finance/centralizing-log-mana...

I believe that this is one of the big tradeoffs that you make when choosing to go for a microservices and event driven infrastructure.

Regarding the original post question, we are indeed going all in on an event-driven infra, and so far it has been going not too bad, happy to answer any extra questions.

I'm not sure you've understood EDA if you're suggesting that good logging is an alternative.
It isn't an alternative, it is a necessary part of understanding the system. You get an event and you have no idea where it came from. That is great from a composability stand point, anything can become an event emitter. On the other hand if that event comes in with bad data, you have no way to correct the issue because you have no idea where the event came from. The more event sources become disassociated from the event processors, the more important logging and traceability become. Perhaps your component ships an event and the downstream event processor doesn't do what you expect. Without good logging you loose the ability to treat the down stream as a black box, you will have to dive into the code to figure out why it is rejecting your events. Where as a simple "Dropping event x for reason y" in the logs is incredibly useful.
Thank you for saying the things I didn't think I needed to elaborate on :-)
Your phrasing indicates you positioned it as a replacement/alternative.
"As a trivial example, everybody talks about dead-letter queues but nobody really explains how to handle messages that end up in one."

For us, it's either a function that will retry the messages after some time, or manual intervention.

Our department recently said we need to move to even driven architecture for one of our processes that currently runs in a batch. They want us to load data into EMR from an S3 bucket populated by Kinesis. Their suggested implementation is simply to run the batch job more frequently instead of once a day... sorry guys, but that's not even driven...

I suggested maybe just setting a trigger on the S3 bucket and hook it up to Glue, since that would actually be event driven. They said 'no' because they don't want the load to EMR or Glue to run too frequently. I guess that makes sense (not that familiar with the ETL tech), but it sure doesn't make sense to call it event driven.

> For us, it's either a function that will retry the messages after some time, or manual intervention.

What does manual intervention look like? Are all downstream consumers blocked until the DLQ is emptied or do all consumers know how to deal with late-arriving events?

Manual intervention could be moving them back to the main queue to try again, or looking up the data that failed like if the customer that is being transacted on exists. Most systems don't have an issue processing late messages. The ones that do would have to have some sort of retry/ingress process.
>What does manual intervention look like?

A ticket or an ops alert depending on severity. The cause gets fixed and messages are retried or dropped depending on your business case.

>Are all downstream consumers blocked until the DLQ is emptied

No, its a diffent queue. The point is that you eventually pull them into the DLQ so you aren't wasting all your resources on failing messages.

>do all consumers know how to deal with late-arriving events?

Hopefully, but an outage is an outage. You're way past happy path at that point.

> Are all downstream consumers blocked until the DLQ is emptied No other service should know about the state of another services DLQ

> do all consumers know how to deal with late-arriving events In my approach in architecting event driven systems, this question is meaningless. For me some of the core tenants (for me) of architecting these systems are

1. Message ordering should not matter 2. Message delays should not matter (no temporal coupling) 3. Message replays should have no side effects

This means that Service B should have no knowledge of what happens in Service A. If something in Service A fails and gets sent to Service A's DLQ, it should have no impact on Service B. This often involves rethinking business requirements and processes to account for workflows that get interrupted - I've yet to find an insurmountable issue.

If however, we are talking about a message broker trying to deliver a message from Service A to Service B, failing and forwarding to Service B's DLQ, then there is a very simple solution. Have your message broker deliver messages to queues. Service B then processes and handles messages from the queue rather than having to immediately handle all incoming messages from the broker. This protects against a lot of failure modes. The only thing(s) that arrive at Service B's DLQ are exceptions thrown internally in Service B

Monolith is easier to handle. With microservices, any network connection could break, you need a lot more code to handle all that complexity and orchestration.
This depends upon the size & complexity of your monolith & how your development teams work. Essentially it's a trade off about which becomes most work and causes most issues.
Monoliths vs. services is like biologists arguing about cells vs. organs.
It's kinda like using Lisp. It may be great, but it's harder to find friends :-)
Would be very interested to know!
Where I currently work we are all in on event-driven architecture. For our DLQs, we have alerts on when the queue is growing in size or if messages are in the queue too long. When those alerts come in, we manually move the messages back to the normal queue for reprocessing and if they get DLQed again after that we will look into the reason it is failing.

One of the benefits of this architecture for us is the ability to easily share information between services. We utilize SNS and SQS for a pub/sub architecture so if we need to expose more information we can just publish another type of message to the topic or if we need to consume some information then we can just listen to the relevant topic.

There are two big issues that I've run into while at this company. One is tracking down where events are coming from can be a big pain, especially as we are replacing services but keeping message formats the same. The other big issue is setting up lower environments (dev,qa,etc) can be difficult because you pretty much need the entire ecosystem in order for the environment to be usable, which requires buy-in from all teams in the organization

I guess it's still harder to track down event emitters, but have you tried using bitbucket or GitHub code search to search all of your repos at once?
Yea, I have use GitHub search in a pinch and sometimes it is helpful enough to show me exactly where to look. Unfortunately, though, there are several events we emit that are many layers of string concatenation, so GitHub search may narrow it down to 4 or so places and I have to manually go from there.
Solution for that is using equivalent of user-agent - write it to message headers. Not sure if SQS supports it like Kafka.

I'm really surprised that not a lot of companies use this.

Do you have any control over the individual services?

One way to ease some of that pain is a standard library to obtain your keys and topic and publish metrics when things are published and consumed, or at least logs on startup.

It's a pain to get buy in, and 10x harder to keep it updated. But if you can solve the problems around getting names and secrets and stuff, folks are usually open to the conversation at least.

Thanks for your answer, it really helps.

Does moving the DLQ messages back to the normal queue mean that all consumers can deal with out-of-order scenarios?

if you're using a queue like SQS and expect it to be ordered and exactly-once, you're in for a lot of surprise. If you need ordering, use a stream/log like Kinesis or Kafka.
Exactly-once is not a requirement but ordering is. I had Kafka and Kinesis in mind when writing this question but just in case you haven't seen it, there is a way to get ordering guarantees using SNS and SQS:

https://aws.amazon.com/about-aws/whats-new/2020/10/amazon-sn...

Does that really guarantee in-order processing, or just that messages can be picked up in order. If you have multiple consumers on your queue and consumer A picks up message x from the head of the queue then consumer B picks up message y next, it is possible for y to get processed before x. Maybe consumer A is slow (gc pause?) for some reason, and now we are processing out of order, even though the queuing infrastructure does not see it. If you truly need to guarantee strict processing order (x must complete before you start processing y) I think you may need to build that into your app. Or, I misread, which is very possible.
The overhead with this architecture can be cumbersome. Which is why most successful deployments of it tend to be with teams that have embraced full stack serverless. Recommend exploring that community, plenty of event driven systems there.
Interesting, does serverless help with data ownership?
100% this. Embracing Serverless for me meant embracing distributed, event driven systems. Serverless and Event Driven architecture go hand in hand. I actually find my productivity is higher than in days of MVC monoliths
"Everything looks like a red thumb when you're holding a golden hammer."

Events are a part of a greater whole. It's a tool that you can use to solve certain data flows, but not all data flows. When you start taking more liberty with the word "eventually," you are almost certainly in a realm where event-driven makes the most sense. CQRS is a pretty good example of using many architectures (including event-driven) under a single greater architectural umbrella, and the thought patterns it introduces you to are incredibly useful. But no architecture is gospel, not even close.

Any "pure" architecture is the tail wagging the dog. The problem comes first, the solution comes second, the architecture comes third.

I have at a few companies now.

It's great for processing data that goes beyond a single database call, data formatting and presenting something on a page.

If you're chaining multiple microservices together you've made a very sloppy/poor mans version of this. (People tend not to account for downed services, maintence updates, client durability, etc) When you bring in technologies like Kafka to orchestrate this, you'll end up with a more reliable system that you can fix if something goes wrong. This more changes the way you think about data and how you present it. Also, it'll increase your uptime because your service's SLA is isolated from what you're processing. (Your service and the persistent storage that is storing state is what people see.. the data being out of date is something you should account for)

Schema changes: Generally you don't have that big of a deal because multiple applications get started at once. You should have system level tests to catch that before you go out. Also, application smoke tests help as well. As long as you picked a durable message queue with a framework that'll crash on error, you can fix that, bring up the fix and continue processing through.

Dead letter queues: It's more about how you architect more than anything. This is something you should plan for.

> When you bring in technologies like Kafka to orchestrate this, you'll end up with a more reliable system that you can fix if something goes wrong.

It depends...

If your services are servicing typical user requests, and expect responses in O(<1s), then eventing architectures are exactly the opposite of what you want. Using message brokers (Kafka) as an ersatz network layer is the path to sadness -- you want backpressure, RPC semantics, etc.

https://programmingisterrible.com/post/162346490883/how-do-y...

If you're doing ETC/event semantics, the game is different. But very few orgs actually work that way.

> If your services are servicing typical user requests, and expect responses in O(<1s), then eventing architectures are exactly the opposite of what you want. Using message brokers (Kafka) as an ersatz network layer is the path to sadness -- you want backpressure, RPC semantics, etc.

Correct. Most cases you need to see what the current stored state is. Processing can happen in the background and update that state without being requested.

I'm assuming that you're architecture your system as having an ingest service that brings everything into the topic, multiple apps that'll refine/enhance, the data, and then an app that sends it out to a more permanent storage. Your service endpoint should look at the premant storage to make a response. (Or read and post out updates via a websocket if you want frequent updates to an existing page)

Kafka is not a network technology.

(comment deleted)
Got a list of books for reference?
* Designing Data-Intensive Applications

* Monolith to Microservices

* Streaming Systems

* Designing Distributed Systems

* Building Event-Driven Microservices

* Kafka-Streams in Action

A comment in this post also suggests "Building Microservices" but haven't read it.

I work in semiconductor manufacturing, it's a very common model. I have been in about eight fabs around the world that use it quite successfully.
>"fully embraced"

"fully embraces" / commits - not a very wise thing to do. Event driven arch is one of many tools at your disposal. It is awesome for some things and not so much for others. You can't use single tool / approach for everything and expect best results.

I disagree. There are incompatible design disciplines and architectures.

100% functional code is a lot better than 99% functional. 99% functional is a mess. 100% functional gives strong guarantees.

You can split up code horizontally (MVC), vertically ("apps"), but of you mix the two, you get a mess.

I am working on a system which is 100% event-driven, and it works well in the context of this system, which is primarily about realtime data analytics. There are systems I'd build without any event-driven code either. There are hybrid systems too.

There are architectures which are compatible (e.g. structured and OO), but many just aren't. Event-driven is one of those things which adds a lot of complexity to any system. Fully-embraced, that can pay dividends. A little here a little there and you have most of the cost and little upside.

Window systems are classically event-driven. Especially earlier single-thread ones from Microsoft.
Yes. I am interested in knowing what was done before event driven architecture to make robust asynchronous systems?
You mean Erlang?
erlang is great (and has events) but it is not an "event-driven" system. It is very easy to write an "event-driven" system in erlang, though. (I have done CQRS, from 'scratch' in elixir + psql, which I believe is a flavor of event-driven system). Was very straightforward.
Are there any rules of thumb where such an architecture should be considered? >X TPS? >Y milliseconds per txn? >Z milliseconds between write and subsequent read? Eventual consistency OK?
It depends! FWIW the spots I've used event patterns most often are high write loads that benefit either from fan-in micro-batching before hitting the data stores or polyglot systems where a client/system event need to fan-out to multiple systems and we want to insulate the producer from downstream latency/outages.
I saw a number of investment houses (mostly sell-side) do this. This was in the age of ESB (Enterprise Service Bus.)

The architecture made sense since events (new trades or quotes) dictate a host of downstream activites, which often need to be near-real-time reduce divergence.

Much of our codebase is python microservices communicating via Kafka. Once you get past the hurdle of getting kafka connected it's pretty reliable. We have a shared library for producing and consuming so we don't need to reinvent the wheel for new services. We also dump the resulting messages as rows into a database. It works very well.
What do you use to encode the messages? Avro, Proto or maybe just JSON? Most of the books and articles encourage the use of Avro, but I feel like the support in python isn't nearly as good as in other languages. Almost every solution operates on dictionaries instead of classes with annotations which doesn't seem like something you would want to use for a Py project in 2021...
We use Json in our Python projects and Avro in our Java projects. We have considered Avro, CSV, Protobuff or CapnProto for the Python projects but it's never been enough of a win for us to prioritize it. We switched to Pydantic from dictionaries.
Thanks for response! That's basically what i ended up with! Avro whenever possible and for python stick with pydantic. Except that at the same time i want to keep avro schema for those pydantic models and just convert them with fastavro, because then i dont have to rely on the quality of code/schema generators. A little sceptic about that though, as keeping the same schemas in 2 different technologies compatible and double (de)serialization might be troublesome.
Like others have said, it is just one tool in the tool box.

We used Kafka for event-driven micro services quite a bit at Uber. I lead the team that owned schemas on Kafka there for a while. We just did not accept breaking schema changes within the topic. Same as you would expect from any other public-facing API. We also didnt allow multiplexing the topic with multiple schemas. This wasn’t just because it made my life easier. A large portion of the topics we had went on to become analytical tables in Hive. Breaking changes would break those tables. If you absolutely have to break the schema, make a new topic with a new consumer and phase out the old. This puts a lot of onus on the producer, so we tried to make tools to help. We had a central schema registry with the topics the schemas paired to that showed producers who their consumers were, so if breaking changes absolutely had to happen, they knew who to talk to. In practice though, we never got much pushback on the no-breaking changes rule.

DLQ practices were decided by teams based on need, too many things there to consider to make blanket rules. When in your code did it fail? Is this consumer idempotent? Have we consumed a more recent event that would have over-written this event? Are you paying for some API that your auto-retry churning away in your DLQ is going to cost you a ton of money? Sometimes you may not even want a DLQ, you want a poison pill. That lets you assess what is happening immediately and not have to worry about replays at all.

I hope one of the books you are talking about is Designing Data Intensive Applications, because it is really fantastic. I joke that it is frustrating that so much of what I learned over years on the data team could be written so succinctly in a book.

When you say you used a central schema registry, did you have a single repo containing all topics and schemas?
kafka has a 'schema registry' service. Typically they use avro/json to define the schema of each message. Think they added a couple of new types in recent versions. When you define your consumer/producer you also tell avro what the schema registry service URL is. Basically it helps you keep your messages in spec. It also has the ability for versioning so you can have different versions of messages in flight on the same topic. So if you add a new field it is fairly trivial for both sides to know what is going on. If you remove a field then it becomes more tricky.

Now what goes into that registry is typically in some way version controlled. That is for 'rebuilding' a clean environment if needed. Or part of a CI/CD system.

Thanks for the reply. We're using a schema registry but we generally have our schemas spread across multiple repos depending on who owns the topic. I was wondering if they centralized all their schemas under a single repo. I'd like to do this, but I wanted to get some other opinions on the subject.
I have seen several approaches.

1) the producer repo owns it. This has the benefit of only the producer makes those things and it is side by side. Downside if you have more than one producer repo with the same schema. When the producer code starts up it sets up the schema.

2) central repo. This is nice however tends to make it if you are using a micro service style more of pain to deploy as they all sorta need to move together. But works very nicely with mono repos. You can make it work with micro but it takes a bit more thinking. This turns your deploy into a two step process 'schema first' then code. That can work but again 'more thinking'.

3) consumer owned. The consumer basically says 'i will only grab these anything else I will consider error'. Works ok if you have 1 producer group and one consumer group and is effectively #1. But with several consumers it becomes a 'cut and paste job'. Or something may get out of sync, etc. When the consumer starts up it pushes the schema.

What I found best to get someone to decide is to figure out what is your version upgrade cycle. Is it one thing at a time? If it is 'everything goes every time'? Who 'owns the schemas?' So different styles will match your deployment process more than anything.

Evan can correct me if I'm wrong, but I believe it was one centralized service with all the schemas. You could view older versions and make new versions via the UI.

It was pretty user-friendly and managing schemas was straight forward. Haven't done anything similar since so no comparison point, but I thought it was fantastic and improved data quality a ton.

Thanks for your detailed answer, really appreciate it.

Two follow up questions if you don't mind me asking, even though I understand you were not on the publishing side:

1. Do you know if changes in the org structure (e.g. when uber was growing fast and - I guess - new teams/product were created and existing teams/products were split) had significant effect on the schemas that had been published since then? For example, when a service is split into two and the dataset of the original service is now distributed, what pattern have you seen working sufficiently well for not breaking everyone downstream?

2. Did you have strong guidelines on how to structure events? Were they entity-based with each message carrying a snapshot of the state of the entities or action-based describing the business logic that occurred? Maybe both?

And yes, one of the books I'm talking about is indeed Designing Data Intensive Applications and I fully agree with you that it's a fantastic piece of work.

I am not the author of the original message, however, I also recommend "Building microservices 2nd edition" if you are trying to answer such questions
Thanks for your recommendation, I pre-ordered it =)
For 1, no example really comes to mind, but i guess there could be cases where a service went from publishing an event with all of its related data, then split into a service where that becomes more expensive to do (like that data is no longer in memory its behind the api of the old service). In some cases you can have very simple services that consume a message, make a few calls to services or databases to hydrate it with more information, then produce that message to another topic that the original consumers could switch to. More commonly though if the data model is making a drastic change where the database is being split and owned by two new services, you will have to get consumers in on the change to make sure everyone knows the semantics of the new changes.

For 2, it completely depends on the source of the trigger. The first event in a chain probably only has enough information to know that it should produce an event, usually as quickly possible, so no additional db or api fetches. So you might get something in the driver status topic that contains {driver_uuid, new_status, old_status}, then based on what downstream consumers may want to do in response to that event, you may need more info, so you may get more entity information in derived topics. Even pure-entity-based messages would have needed a trigger, so in our topics that tail databases, you may have the full row as a message along with the action that occurred like {op: insert, msg: {entity data… }}.

Thank you so much for your input on this topic, very informative answers!
Yes - I work in an investment bank, we try to do millisecond-level latencies for our order management system that sends client orders to the various exchanges(for sub-milli we use FPGA but it's very expensive and only for some clients).

It works alright (like you I hated it all before, coming from more amateurish implementations). It's slow to change (adding a new event type can take years before it works everywhere), failures can happen - for instance if the message parsing library has a crushing bug and an extendable attribute zone has a poison pill that never appeared before - well nothing you can do but manually editing the event source.

What it brings us, I suppose, is that every micro service is single threaded, all events are well-recorded , we use multicast to transfer them from one sequencer to all consumers so we just need good routers and TCP-level message building - it's very barebone to keep it fast, extensibility for us is mainly on adding more services around a core stream that we don't really need to change all that much (we do a lot of regulatory validations, data analysis, the odd scale out for a round-robin compatible process - not all of them are, some need to see all events, for instance for cumulative exposure calculation - client stock exposure on several markets for risk-based decisions).

It also avoid latencies like in state-request based systems, since each service will build its own state machine. We make a lot of money on this system, so we can hire hundreds of people around the world to maintain it.

At this point I dont see how to do it better (5ms round trip to the client if low validation, 100ms if crossing seawater to an exchange with short selling validations) without events, but I know very well that doing it for simpler flows that are not latency-constrained will probably result in heavy cost and low gain. I would never recommend it for people who aren't already struggling with a fully-fledged business implementation that makes money they want to accelerate.

The problems we face are:

- it's slow to evolve the very core of the system

- we need perfect ordering, there will always be time wasted at the sequencer to transform unordered "commands" for the various services into perfectly ordered events

- testing and debugging is an art that takes time to acquire: I can now, but the task seemed daunting when I started - how to spin up the minimal surface of services to make a valuable replay, how to make static configuration reproduce production's behavior exactly so that all services behave the way you want to reproduce if you're investigating a sequence-based issue (rather than a function-based issue)

- it takes up to a year for a new Java dev to get productive on such an exotic mindset, but it's also because we do no intraday malloc since we cannot afford a GC in the middle of a client order

- management cannot understand why they can't cut cost using the cloud, virtual machines, vendor databases etc. Even in a company that makes billions over 20 years on this system, we still can't explain it in a way that sounds valuable vs its cost. Because its cost probably is extremely high, and can't be outsourced by hit and run consulting managers before being brought in-house again. So we're not like the most popular dev, we're the slow and expensive ones :(

Hey thanks for you answer, this is pretty informative.

I am basically where you began i.e don't really know what an actual event driven system actually looks like(as in something implemented properly).

1. Could you please tell me what according to you was a major difference between the more amateurish implementation vs the proper implementation(I know this is quite a general question to ask, sorry about that).

2. Since you've seen what proper looks like when it comes to event driven architecture, can you perhaps suggest some resource(could be a book, could be code base) which according to you is the closest someone can get to understanding how a proper event driven system would be implemented.

Operational support is more interesting with this kind of an architecture. Dealing with message queues and all that can be challenging for a traditional organization.
Not for a company, but I've embraced it pretty hard for my home automation. It's sort of the hammer I hit everything hard enough with until it looks like a nail by making everything go through the MQTT broker. The website? A static json blob describes interesting MQTT topics, and opens a MQTT over websocket connection to read/write any state. Zigbee, et al.? Translate to MQTT. Reporting? Daemon that listens to all topics and dumps it in a Sqlite database to be queried at my leisure. Events like sprinklers on/off? Python scripts in cron jobs that talk to everything via MQTT.

Basically everything that makes fully event driven architectures difficult is ameliroated because the only consumers are myself and my wife, and we literally built up the whole system. Something appears to be locked up? There's a system of watchdogs to kill stuff, all hardware has been designed to fail off into manual control, and we can pick the pieces up at our leisure like when anything else in the house breaks. The last will and testament messages in MQTT are really nice for at least reporting hard failure conditions.

I'll be the first to admit that I would not look forward to productizing it and supporting someone else's house (to the point that I'll probably never do that). It's so easy for messages to make their way into the bit bucket when setting up a new subsystem, and everything is so loosely coupled because of the event system it's almost like it's all "stringly typed". And being both software engineers, we sort of relish in how awful the UI is, even using 98.css.

Would you mind describing the parts that became automated? I can think of a number of appliances that could potentially be api-driven but haven’t researched them yet....
Sprinkler system, water pressure and leak reporting, lights, mains reporting that the meter happens to broadcast, host of sensors for the house plants. Right now working on a control plane for AVB (audio video bridge) devices to give myself a sonos++ that includes video too without using sonos (I'm still salty about their "recycle mode" BS).

After that either: A) hooking up some cameras to the AVB matrix to give myself a Ring replacement too, or B) hooking all the phone lines in my old house to the AVB bridge and creating a kitsch intercom system with an old phone chosen to match each room.

After that, maybe voice assistant, but that seems like a lot. Or maybe hooking a bunch of bluetooth MCUs together on a low latency network to make the house look like a giant bluetooth device so I don't lose my audio when I go outside to do yard work.

The end goal is all the smart home stuff but I don't have to choose between Daddy Bezos and Daddy Jinping.

+1 to MQTT for home automation.

I spent years fiddling with zigbee and z-wave and propietary other things from well-known brands as well as unknown brands from amazon. Nothing ever worked correctly for long periods of time.

Now I have binned all that other crap and migrated to esp8266-based devices that can be flashed to tasmota, and have them all talking via a Raspberry Pi running a MQTT server and OpenHab via docker containers. It is now rock solid in terms of reliability - the only failures come when OpenHab's cloud integration dies, and then I have a backup local http server with a javascript based client that just sends commands directly over MQTT. MQTT really was the missing link for me.

OoenHab has been a pain to use, but that is a different story. I'd not recommend it and I'd ditch it in a heartbeat, but sonfar I am in a "aint broke don't fix it" position.

MQTT at home is a real boon. I have mine chugging away in a raspi in a docker container and that drives everything like the plant watering. I also use it to collect metrics and do alerting with influx+grafana.
Are there communities on Discord/Reddit for what you're doing? I'm interested in dabbling with home automation during my free time.
Are there communities on Discord/Reddit for what you're doing? I'm interested in dabbling with home automation during my free time.
I had experience with a product using an event based architecture at large scale, and to be honest, it was a pain to work with. For example, traceability, or troubleshooting in general, was very hard since events would spawn more events etc. making things much harder to track than expected.

Unless the scale is an issue, nowadays I always prefer a more state-full approach when possible.

My most recent (embedded) programs are almost-entirely interrupt driven, with a main loop containing only a wait-for-interrupt. So, all actions are driven by a GPIO event, RTC or timer wakeup, USB request etc.