CTO @ Skutopia.com I write about Functional Programming, TypeScript, Node, Event Sourcing, and engineering leadership
Nothing here yet.
Typically, I haven't had an example life Sales Policy and Claims Policy within the same domain service. At SKUTOPIA we have Sales Orders, Fulfilment Orders, and Shipments — which all seem like they could be an "order", but they're not. Each iis owned by separate domains — the shipping management domain, the fulfilment domain, and the delivery domain. Each of these are separate (but large) services. In terms of transactional boundaries, we use events, so each event is our transaction boundary. IMO, transactions are a system-level implementation behaviour, not a business logic level implementation behaviour. For example, the startSubscriptionUpgrade controller raises a SubscriptionUpgradeStarted event, which is saved to the database. An outbox process then proceses this event and attempts to perform this upgrade with a payment processor like Stripe. Then if Stripe succeeds, they send us a web hook. In this case, it failed because the card was rejected. Here, we save the payload to a table with the idempotency key as a unique constraint. Then an inbox process reads this stripe message and calls failSubscriptionUpgrade. This raises a SubscriptionUpgradeFailed event. Now, our payments team can track failure rates. Our KYC or trust and safety team can look for indicators of fraud or chargeback risks. Rolling back a database transaction because of legitimate business events is the wrong call here. These events really occurred and your system needs to treat them like expected behaviour, not an exception. Where DO we use transactions? Well in the above example, we use a transaction when we take a lock on the cursor. We then update the cursor position within this transaction. We want to guarantee that if an unexpected system error (not user error) occurs, then our system returns to the most recent valid state. So we rollback on thrown exception (we return all expected user outcomes).
For 1: We don't want this exception to bubble. Say the service we're messaging is down, in that case, the throw will prevent the cursor from moving past its current point. Then we unlock the cursor, and on the next tick, we try the same message again. This will keep happening until the service comes back up. That's why we also want alerts to trigger if there are too many errors or the cursor falls too far behind its target. The number of records retrieved each tick is our queue depth, so you could log that and setup an alert. For 2: The error will terminate the current node process. If you are running it in the same process as the web server, as in my article, then yes CR, K8S, ECS, etc will terminate the pod and start a new one, which is intended.
If the database throws an exception, then something is seriously wrong with your application and you want to fail fast and hard. Let the exception bubble all the way up to your route handler, return 500, and log the exception. You need to differentiate between user error and system error. User error is an outcome you want to handle and include in your type system, while a system error is an exception you want to throw. I talk about this more in https://antman-does-software.com/stop-catching-errors-in-typescript-use-the-either-type-to-make-your-code-predictable If you mean "what if the row doesn't exist" well that's easy, have your database repo return the Entity or undefined, and have the deriver return an appropriate outcome if the result of the lookup is undefined.
Reminds me of these couple of really telling moments in Robert Cringley's "Triumph of the nerds" where he goes from reciting damaging stereotypes about women in tech to almost immediately interviewing or referencing women in tech. E.g. this moment , where he's saying "this is boy stuff!" and then the 30 seconds later he's interviewing Christine Comaford, CEO of Corporate Computing International, who is talking about relational database design... but first he has to ask Douglas Adams for his two cents? As if he's more qualified... Or this sequence, where he basically spends a lot of time on the " Men love coding because women are illogical! " nonsense, then shows footage of women programming the ENIAC , and then cites Grace Hopper's invention of COBOL ! Some rather wild mental gymnastics must have been going on in the minds of every man involved in that documentary. Somehow it manages to reference so many moments when women were instrumental, crucial contributors to the history of computing, and yet rewrites history so as to erase women's place in it then and now 😡 AND STILL, as made evident in the comments on YouTube, computer science lecturers are still recommending this documentary to cohorts of budding programmers! As if it isn't feeding them a harmful script right from the beginning of their careers! Infuriating.
Yeah embedding the config in the application code is definitely a trade-off! Limiting it to dev/prd in the code has the benefits that it is very explicit what the expected production behaviour is for all developers working on main, all the way through to the InteliSense in their IDE. On the other hand, it reduces flexibility in terms of deploying ephemeral environments. However, I find that use case comes up much less often -- the idea of deploying a new environment for every open pull request for example, doesn't seem advantageous. Doing so would slow down pull request cycle time, which naturally leads developers to make bigger pull requests, which further slows the time to close each PR while also increasing the risk of change fail. The case where ephemeral environments have the most benefit is in planning for large migrations, where teams may need extensive exploratory testing and/or rehearsals of runsheets. In those instances, adding additional config is comparatively trivial.
I avoid the enum type at all costs. They're poorly implemented in TypeScript, they have unexpected behaviour at runtime that can catch developers off guard, and they're overly complex. It's very easy to misuse them, and aligning a team on one way of using them is difficult. Instead, it's easier to either use union types, i.e. type MyUnion = 'FOO' | 'BAR' | 'FOOBAR' ; or as const objects const SOME_CONSTANTS = { FOO: 'SOME_VALUE' , BAR: 'OTHER_VALUE' , } as const
Thanks for the question! I think I wrote this entire article in a couple of hours, plus some proof reading from my partner, writing the code maybe took up 25% of that time? These functions are basically muscle memory for me now. By repeating the pattern over and over, I don't need to burn brain cycles remembering the pattern, I only need to think about the particular requirements of the business case being implemented. This is the value of the practice of Kaizen. Have some exercises you repeat regularly, so you can internalise common patterns. Each time you do it, you will also make little improvements. Then when it comes time to do it for real, you're not practicing in production on company time.