Many of these practices are popularized by Google/Facebook/Amazon but don't make sense for a company with 100 or even 1,000 people. I try to focus on whether a practice will solve a concrete problem we're facing.
Switching from Hadoop to Spark was clearly a good idea for our team, even though it required learning a new stack, but there isn't a strong reason to switch to Flink or start using Haskell.
Agile makes sense when your main risk is fine-grained details of user requirements, but not when you have other substantial risks, such as making sure a statistical algorithm is accurate enough.
Microservices probably reduces the asymptotic cost of scaling but add a huge constant factor.
Relational databases are the right choice 95% of the time, non-relational stores require a really specific use case.
TDD is good for fast feedback in some domains, but for others, manually investigating the output or putting your logic into types is better. E.g. a lot of my time comes from scaling jobs that work on 10gb of data but crash on 1tb, TDD is not that helpful here.
Continuous integration mostly makes sense when you're making a lot of small changes and can reliably expect a test suite to catch issues.
In short, ask the question "when is practice X useful?" instead of "is practice X a good idea?"
At the last company I was at, our search microservice was fast (average response was well under 100ms) and it didn't crash once while I was there. At a larger company, this may not be an accomplishment. At a startup, this is the bees knees.
Meanwhile, the rest of our codebase (a monolith) crashed every few days for one reason or another. We had an on-call rotation not because that's what you're supposed to do, but because we actually needed it.
Now I'm not saying that microservices make sense for everyone. In general, I agree that they are used incorrectly. Microservices are hot and software developers, generally speaking, like to use hot technologies. Yes, moving to a microservice was costly. We had to re-write a lot of code, we had to set up our own servers, and we had to get permission from the guardians that be to do all of this. But, for our use case, and I assume there are other use cases too, the benefits of detaching ourselves from the company's monolithic codebase far outweighed the costs for doing so.
TL;DR No argument is the end to every conversation. Few things are so black and white.
Sooner or later you get a feel for which bits are becoming at least API stable and could run independently. That's when I split them out.
Do it too soon and you end up choosing the wrong boundaries and tying yourself up in knots, do it too late and your monolith can become a mess that's difficult to detach the pieces of.
Another option is to start with an umbrella app (Erlang/Elixir/OTP). It can run like a monolithic or ... nano-services (I suppose) within the same monolith. When it is time to split them out, it is easier.
It does assume that you either start with devs familiar with OTP or you have generalist devs that can pick things up quickly.
True. There's another thread in here somewhere talking about premature generalization. I think that's what you're getting at with "Do it too soon and you end up choosing the wrong boundaries".
TBH microservices do a good job of making you much more dependent on your tools, and selecting the wrong tool for the job won't become clear until you've used that tool for years.
At the last place I was at. We had a micro serviced monolith. I can't even begin to describe that thing in common engineering terms. (note: it's better than it seems).
You can get that benefit by dividing your system up into libraries with defined, documented, tested APIs. There's no need to introduce all the complexity and failure modes of distributed systems just to force good design.
When you need to scale, then you can easily throw your libraries behind an RPC framework and call it microservices, but there's no need to pay that cost until you actually face that problem.
Just putting libraries that were never designed to scale up behind RPC usually won't help you scale. These libraries tend to work with mutable, stateful objects and don't have any groundwork in place for partitioning.
That doesn't mean you can't scale up from a monolith (even one without clean interfaces) - every startup growth story is a testament otherwise - but it's never as as easy as strapping an RPC layer over your library.
One caveat is that if you need to fix a bug in your library in an API-compatible way, you can't reach into all the codebases that are using your library. You can deploy a new version of the microservice, though.
I mean, you _can_ if you organize your code such that you can. For example, Google's monorepo lets maintainers of a library find all internal usages and fix them. This is one of the benefits Dan Luu notes in http://danluu.com/monorepo/.
I think he means that you can't force all teams that use your library to recompile and pickup the updated code, while if you deploy it as a service, you recompile and redeploy and everyone talking to your service gets the most up-to-date version.
This is a real problem - I recall that Sanjay Ghemawat et al was working on it when I left Google, though I dunno if the solution they came up with is public yet. It's unlikely to seriously affect you unless you're Google-scale, though, by which time you've probably divided everything up into services and aren't taking advice from the Internet anyway. For companies that are a few teams working on a single product, it's easy enough to send a company-wide e-mail saying "Rebuild & redeploy anything that depends upon library X", and if you're doing continuous deployment or deploy only as a single artifact, the problem never affects you anyway.
You were initially replying to a suggestion explicitly qualifying this with "for small shops". Yes, you most definitely can force all teams that use your library to recompile and pickup the updated code - and it doesn't mean "a company wide email", a realistic scenario would involve standing up, pointing to a specific person and saying "Bob, the new version of my library will also work better for the performance problems you had, pick it up whenever you're ready"; and knowing that it's an exhaustive list of people who need to be informed.
For starters the vast majority of code is developed in-house in non-software companies. The vast number of products are a single team working essentially in a silo, not "a few teams working on a single product".
When people are talking about small companies, it's misleading to think "smaller than Google". Smaller-than-Google is still an enormous quantity of development. Enterprisy practices make much sense in scaling software in companies that are smaller-than-smaller-than-Google. if you hear "small company", think multiple steps further from that, a smaller-than-smaller-than-smaller-than-smaller-than-smaller-than-Google company.
> you recompile and redeploy and everyone talking to your service gets the most up-to-date version
Sure, but if you do that in place it will still break stuff that assumes it works like the last version, and if you do a versioned API or the like you still can't force all teams to adopt the new version.
If you need to change your microservice's API in a non-backwards compatible way, you have the exact same problem plus significant operational complexity.
Which is basically what you do for a traditional library as well. Tweak the header so anything being recompiled against it gets a different function signature. Then old apps continue to work, and newly built apps get the fix.
Moderately ironically - this is a place where dynamically loaded libraries are particularly well suited. So long as the API hasn't changed, the library can be patched independently of all the other compiled code.
Of course, there are other limitations this imposes, but it does make it very simple to deploy a new library to all code which uses it.
Nothing about splitting your app into microservices _forces_ a good design. I've never seen microservices with well-defined seams. Every time, knowledge "leaked" between the apps, and any non-trivial change to the app required updating multiple repos, deployment synchronization, etc. Microservices are a tremendous burden that the vast majority of companies will not benefit from.
Except splitting into microservices is an unnecessarily complex design choice. That's almost always worse, and the cognitive load comes in when you now need to figure out how to get this stuff right. The scaling benefits also require that you get it right, small flaws in your system become massive issues.
If you separate components wrong in the same code base is an easy fix. If you get them wrong between services you have s much larger problem. I'm not sure why you'd be more likely to get that right with services than within the same code base.
"Logic" is vague and there a several layers you can implement this before even thinking about microservices.
It can be as simple as a simple class, or maybe a larger class as a single-file service, or an entire namespace with a several classes, or a separate library easily referenced. All the "logic" split benefits without the ridiculous hassle of microservices.
I think this is actually a failure in mainstream programming languages, which make it far too easy to reach across what's meant to be a defined subsystem boundary and meddle where you shouldn't.
Definitely agree – the polyglot aspect can also be useful for companies where different parts of their problem fit different tools.
However, exercising proper software discipline and using languages with good/existent module systems, like OCaml or Go, can lead to the same modular results without the fixed overhead. If you don't have a full-time ops person or team, you almost always have no business running microservices.
> Relational databases are the right choice 95% of the time, non-relational stores require a really specific use case.
Relational databases are great, but I spent large parts of my life as a developer writing layers converting to/from SQL and later ORMs. There's a huge gain in just not translating data. I know Postgres (and others) deal with JSON, but I can't escape the feeling it's a bit shoe horned in there – basic SQL statements have strange new operators like ->> -> #>>.
Relational databases are great for, well, relational data with strong consistency requirements. The popularity of the original MyISAM tables without integrity checks baffled me at the time. Why spend time marshalling data in/out of table form when you don't gain the benefits of a RDBM?
Not doing data translation saves _a lot_ of time. Plain key-value stores are amazing, document stores like Elasticsearch are great, ultimately the choice comes down to requirements and time saving is often a very heavy argument, especially for small companies/startups.
Things such as joins, transactions, and means of enforcing data integrity are useful when solving a whole slew of problems. Not to mention the tooling and community you benefit from when you use a common RDBMS.
I never found data translation/serialization to be a big pain (just rely on a framework/lib that does it for you). It's a bigger pain to hand-roll joins that would be a one-liner in SQL or deal with issues that arise from having your data (unnecessarily) reside in many systems.
I hear this data-integrity thing a lot but I don't run into these problems myself. I think it might be a functional-programming thing. It's much easier and safer to declare your constraints rather than trying to enforce them. If you aren't in a functional language I can see why you'd want to reach out to one but SQL in a separate process is just one of the options.
> Things such as joins, transactions, and means of enforcing data integrity are useful
If the domain needs transactions I'm already speccing them regardless of general usefulness. Yes all that stuff is useful, but all abstractions have a cost which isn't free just because it's hidden in the DB.
For a problem that didn't need transactions, but for which they were useful, why would you automatically want to couple the solution with your storage layer? If you're looking for the ability to express business logic clearly without cluttering it with error handling, for instance, software transaction memory would probably be a better level to work at.
Considering we speak about daemon software (services exposing some API to readers and writers) that provides CRUD behavior to the user (end-user or other developer), isn't that nearly always the case to guarantee write access to concurrent writers without the risk of crippling your data?
Furthermore I am not sure how this relates to FP.
> It's much easier and safer to declare your constraints rather than trying to enforce them.
But that is a strong point of RDBMs implementing SQL. You have some kind of schema (think type) and use selected functions (select, update, delete, create, etc.) to transform the data.
I understand, but you can't just throw a DB at it and walk away. For instance, which DB? Setup how? Running on what type of hosts? What topological requirements does this have? How much does of a multiplier does it place on your data load.
I'd definitely use a trusted DB for storing bank accounts. The consistency is pretty much the first requirement and the data maps perfectly to tables. And 7B checking accounts isn't that big, compared to some problems so it'd probably scale pretty well even worst-case.
But I probably wouldn't for an MMO. Or at least, it wouldn't be where I stored every little thing going on around them - just the events (xp and gold earned) that they'd freak out about if we lost. But even just a log-structured DB would work well for that.
If there's no contention for a resource (in the bank case - the value in the account) there's much less reason for a transaction. I want the system to make its best effort but I don't want to wait around for the message if there isn't anything I can do on a failure anyways.
I start with text files and for most purposes I do not bother with anything else.
Next is a key/value store. Simple.
Relational databases carry large overhead in translating data (as above) and also in design and maintenance of the structure and getting data into and out of them. I spent many years with them, like them a lot, but they are too much complication for most purposes.
Even with relational data RDBMS are only good if you are not certain of how you will be accessing the data. In most cases you are sure.
I am constantly stunned how people reach straight for MySQL or Postgres when flat text files with grep would work just as well and be much quicker to implement
You can implement key/value stores in RDMS's too. It only take a few minutes to create a key/value table in most databases, combined with a few minutes in your favorite language to map it to an appropriate get/set routine. I find this particularly useful for variable attributes against another table, especially when its really a "foreign index, key, value" table. That way its still possible to join the values to other parts of the database. This paradigm really lends itself to multiple FK/key/value tables, where each one extends another particular table.
All that said, doing this requires careful thought, and DB normalization when its discovered that there is a 1:1 relationship between rows in a table and a particular key/value table. So, its not something that should be taken to extreme, but I find it aids in quick development, as every time you discover you need to store another piece of data for some edge condition it doesn't require lots of DB normalization. Also, I wouldn't really consider making the "value" field a blob, rather a very limited int or string.
TDD is not about the tests, it is about teaching yourself to code better. About forcing yourself to break up your code into small components with well defined interfaces. Something we all want to do but which is hard in practice without a tool to guide you. TDD is that tool.
T(est)DD does tend to make your code look more like you used a functional-oriented language. A lot less mutability, heavier use of first-class functions, etc. Though I tend to prefer languages that are at least somewhat functional (eg. python, go).
Think of the language features you need to be able to program in a functional way, then think about what these languages have. There is a sizable overlap.
If you need a template then think of scheme. It is the best example of a minimal functional language.
In functional, statically typed programmings languages, there is a pattern where business logic, including "actions", is encoded in types. This article [1] gives an example of filesystem manipulations that are encoded in a "FreeF" type.
When business rules are encoded in datatypes, it's easy to check that the encoding and the transformations are complete, and the logic can easily be mock-tested.
Suppose you have some business logic that subtracts the cost of a transaction from an account balance and returns a new account balance. These things are probably integers, but in many languages you don't have to specify that. You write this function, then later your coworker comes across it and passes it a double. You might end up with weird small discrepancies in account balances (or mysterious errors that only happen sometimes) that could be totally prevented at the time your colleague wrote the code via static analysis, if you use put some logic (costs and balances are integers) into the types.
This can be more sophisticated, like "this function requires a sorted list" so lets make a sorted list type, or packaging things up into biz logic types (a cost type that contains an integer instead of just using integers), but you can catch a wide variety of errors with static analysis if you make your code and logic amenable.
Recently our team built a data pipeline: a few large inputs, a few large outputs, a lot of processing in between, a lot of parallelization & working w/large datasets needed. Essentially you could view the entire process as writing one very complex function.
We approached this first outlining the procedure and specifying the types involved, then outlining functions from each type to the next. You could essentially think of our types as tables, so our outline was f1: t1: -> t2, f2: t2 -> t3,...,fn: tn -> t_output (although not quite that linear.)
This let us split up work on the different functions across a team of 6 people. Specifying the input and output types was basically enough to make sure the functions were correct most of the time, and baking the interfaces into types enforced by the compiler made it easy to refactor & coordinate on changes when necessary. Feedback when we made an error was generally immediately available, because IntelliJ would highlight the function that produced an output value of the wrong type, or the compiler would catch it.
In contrast, if we had relied primarily on unit tests to check the functions, that would have made coordination more difficult, refactoring harder, and would have required us to either generate or acquire test data to feed through each function. But this architecture let us successfully build out most of the logic even while we had no access to real data & a different team was working on data ingestion.
This is interesting, do you mind being more specific - what was the data, how big was it, how long did the functions run?
Assuming you are talking about real types and having something like
f1 :: t1 -> t2
and
f2 :: t2 -> t3
you suggest you were able to do
g :: t1 -> t3
g = f2 . f1
which works perfectly well, but is sometimes nontrivial to do for more complex functions, in particular if they are not pure (e.g. they do IO as data is too big for memory) and you do some logging and house-keeping in-between and because of runtime behavior that might be hard to predict.
Does f1 consume all input before f2 can run? Is it "streamed-through", like in `sh`, e.g.
$ find /home/foobar | grep hs$ | xargs wc -l
which is often done as an optimization?
I really like the concept and it works great, but for me it is simpler to apply to smaller constructs and I am still investigating how to apply it to more "business-logic".
Almost all the functions are pure–the only impure functions we use read from or write to a data store. We use Apache Spark, which lets you write pure functions that can operate on data too large to handle on a single box, and it overall works quite well. Eg when designing started this project we wrote something like:
g = f5 . f4 . f3 . f2 . f1
where f1 reads from s3 and f5 writes to a db. Then the implementation work mostly involved breaking these down further, e.g. f1 = h4 . h3 . h2 . h1, where only h1 is stateful and everything else is pure.
Spark is lazily evaluated, and in practice it will stream through many operations–f1 will generally not be done consuming the input by the time f2 starts, although sometimes we force it to for debugging purposes.
Lazy evaluation and the discarding of side effects make logging difficult, which is one of the downsides. There are various monitoring and debugging tools that help but it's still definitely harder than the single machine case.
One thing that bothers me is the 'relational databases are good enough' statement, that is repeated in other contexts as well.
But especially here, where we're talking about reducing complexity, it feels off to me. PostgreSQL and MySQL seem to me like incredibly complex packages. SQL, the language, is not easy to master either; most programmers I meet know mostly basics. On top of that, there's a long ongoing history of security malpractice.
When talking about reducing complexity, CouchDB and Redis are far easier alternatives, in my humble opinion, though they go slightly against 'use the tools developers know'.
The implementation of PostgreSQL is complex, no doubt about that. But if you need strong data consistency and durability guarantees, it provides a rock-solid foundation.
SQL might take some getting used, but it is also not rocket science. It shouldn't take more than a week's study to master the basics. There is of course a lot of awful SQL code out there, exactly because most programmers don't even know the basics. You can do incredibly powerful things in it that would take 10x the code in an OO/procedural language. In my opinion dumping an ORM on top is also not the best way to leverage the strengths of an RDBM.
It is slightly ironic that you bring up security malpractice in the context of PostgreSQL, when in the next sentence you advocate Redis as a far easier alternative. As was recently in the news the Redis defaults were for a long time insecure (google for Fairware ransomware).
> In my opinion dumping an ORM on top is also not the best way to leverage the strengths of an RDBM.
I agree. Unfortunately, the way I see people usually using them is pretty bad - you should not let ORM-generated stuff dictate your business model. Database is a database. A storage layer. Business objects will not map 1:1 to ORM objects. Approaches like "let's inherit from ORM class and add business-related methods", in my experience, lead to total disaster. One has to respect the boundary between storage layer and business model layer.
I'm aware of the different approaches (mostly from reading Fowler's PoEAA), but currently we use an Active Record-style ORM with a few extra features (like Class Table Inheritance) and we haven't found any major issues with this approach. What was the worst case scenario you experienced with the 1:1 approach?
Over the past 5 years I've been in two projects using 1:1 ORM Active Record == business model base approach. One completely failed in part because of this, second is barely manageable, but I managed to save it by moving business code mostly to the outside of Active Record classes.
The problem I encountered in those projects is the mismatch between storage mental model and business mental model, which lead to explosion of crappy code (AKA technical debt). In particular:
1. the classes I need for business model may have initially mapped well to database tables, but over time they stop; business logic and model changes much faster than you'd like your DB schema to
2. since many things in AR can fire SQL queries, you have to keep in mind the workings of your database when doing almost every operation on your model; it's an abstraction leak
3. code shooting off SQL queries is randomly called from all over your codebase; it's harder to keep track of it and, if needed, optimize those queries
I like AR as a convenient API to get data from/to database, but given the point 1., I eventually learned to isolate AR layer as something below business model layer, so that the pattern is that business model is explicitly serialized and deserialized from database, instead of the database being coupled with the logic of your program.
Now I vaguely recall complaining about this before on HN and getting my ass handed back to me by someone who pointed out that these are all ORM n00b mistakes. I wish I could find that comment (pretty sure I noted the link down somewhere). Yeah, I admit - in those two projects I mentioned, we were all ORM noobs. So we've learned those lessons the hard way.
> Approaches like "let's inherit from ORM class and add business-related methods", in my experience, lead to total disaster.
I don't disagree, in fact I'd go further and say that data and logic should not be coupled, but this is the Active record pattern which is far from the only way to use an ORM, most ORM's won't even support this pattern by default.
The best ORM ever, F#'s FSharp.Data.SqlClient. A very thin layer that let's you statically program in SQL in your app. But I typically just use Functions/Stored Procs. But sometimes, for one off things and experimentation it can be nice to write SQL directly in your app.
Relational databases are not simple systems as you say, but they do seem to me simpler to use - especially in the 95% case where a single, large enough machine hosting postgresql/mysql is entirely sufficient.
Key-value stores are "easy", but what I think isn't easy is to reduce your business domain to a simple key-value model without sacrificing promises and gaurantees offered by a good relational database system.
>But especially here, where we're talking about reducing complexity, it feels off to me. PostgreSQL and MySQL seem to me like incredibly complex packages. SQL, the language, is not easy to master either; most programmers I meet know mostly basics. On top of that, there's a long ongoing history of security malpractice.
PostgreSQL and MySQL are very complex, but the complexity is entirely contained. Both are well-tested and reliable, so developers can deploy them without worrying much about them.
I would disagree about SQL being difficult to master. The basics are all most people need, and are not at all difficult to learn. The more advanced stuff (e.g. CROSS APPLY) is not necessarily standard across implementations, and can usually be replaced with application code.
>When talking about reducing complexity, CouchDB and Redis are far easier alternatives, in my humble opinion, though they go slightly against 'use the tools developers know'.
I can't say I'm that familiar with CouchDB, but Redis is entirely inappropriate for most SQL use cases. It's a key value store, and is not meant to do any sort of advanced queries.
Minor aside; I don't think the complex bits of sql are things like cross apply - that's almost no different from a join, especially if you're from a non-sql background where "joins" are typically statements+loop-equivalents and typically hierarchical and ordered. If people have difficulty with cross apply, they're just not trying.
Of course, if you regard sql as something you'd rather not "waste" time on, of course you're going to find those kind of subtle distinctions confusing - sort of like how people think css is difficult.
The more reasonably "complex" bits are the update visibility semantics, i.e. which transaction isolation levels mean what in various scenarios.
That's really complex, and it's truly somewhat unique to sql in that most alternatives simply don't bother trying to solve those problems at all - that can be a bad thing, but it is simpler.
Postgres and MySQL aren't overly complicated for simple use cases.
A lot of other technologies drop complications like foreign keys which you won't miss when you start developing software but it gives guarantees you will miss sorely when you start seeing inconsistent data 6 months in.
The expensive and complicated thing about these is deployment and maintenance. But then, you could instead pick sqlite and switch to big one when needed.
Sometimes it is good to get a pickup truck ahead of time, but often a smaller less versatile car will suffice. But not quite a motorbike.
Go with SQLite if you can get away with it. It's a library, not an external engine, and databases are stored inside normal files, which makes a lot of things easier if you're building a standalone app (as opposed to server-side software).
SQL is well worth its time to learn. It's a good DSL for relational data. Most programming languages used for regular code are not very convenient with relational data. As for its security issues, this is actually simple - one has to respect SQL as a real programming language with its own syntax and grammar, instead of resorting to idiocies like gluing strings together in an ad-hoc manner.
I would say SQL is easier to master than most functional or procedural languages. The true issue, in my experience, is that it is different enough most developers don't want to take the time to learn it beyond the basics, much to their detriment.
I think you're forgetting about practicality and not reinventing the wheel.
I'm going to use a car analogy here. Modern cars are incredibly complicated machines. They're also generally very reliable, thanks to about a century of development and engineering, and (relatively) inexpensive thanks to economies of scale.
If I want to transport myself to work on weekdays, and transport my girlfriend and maybe another friend on a weekend trip, and carry a bunch of groceries once a week, I can do all of that with a standard 5-person car. It isn't completely optimal for any of those tasks: it has more space than it needs for any of them, especially the weekday commuting. For my daily commute, I don't even reach highway speeds because I live close to work, so the engine is seriously overkill.
So I could have a custom-designed vehicle for each of these use-cases. Each vehicle would be a little less complex than my current car. But that's a lot to maintain, and would surely be far more expensive and less reliable, since each one is a one-off, requiring custom design and engineering, special parts, etc., and not benefiting from the economies of scale and engineering resources that a mass-market car gets. So instead, I just go buy a ready-made car and use it, and it works great and I'm happy.
Is PostgreSQL overkill for a lot of uses? Probably so. But it's designed to be used for all kinds of different tasks, and while it may not be quite as efficient for any of those tasks as some custom-designed solution, it's far more flexible, and it's readily available, plus it's benefitted from an enormous amount of engineering and debugging that a custom-designed solution would not. Things like CouchDB don't have nearly the number of users and amount of development, so while they may make sense for some tasks, the fact that PostgreSQL has more lines of code does not necessarily mean it's less reliable, in fact the opposite is likely true, just like an off-the-lot Honda or Toyota is likely much more reliable than some custom-designed car that someone built in their garage or some high-end limited-production exotic car like a Ferrari or Bentley.
The reason for using an off-the-shelf solution is because it's fast and easy and reliable. It doesn't matter if you're not making use of 80% of the features or capabilities. And software isn't like cars or engines; hard drive space is nearly free, and except for certain applications you're not likely to see a significant downside to just using a standard SQL database versus something more tailor-made. The main problem is cost (like with Oracle), but with PostgreSQL or MySQL this isn't an issue since they're free (and Free). It also helps that they use a standardized query language which makes them much more accessible.
I've found that as soon as you change your data model and now need to deal with old data, document stores get just as complicated but require more custom solutions.
> TDD is good for fast feedback in some domains,......
It hears like, "IDE is good for fast feedback in some domains. "
If someone saying that IDE is only suitable for GUI application but not the software he is writing, he probably means that "look at me, I am the old school tough guy." It's about the people dislike the tool but nothing about the domain.
Having loosely coupled microservices has benefits for maintenance as well as scaleability. If you are quickly iterating on a product and have it up in a rough state then you can easily work on seperate parts without having to worry about effecting the whole.
It can go both ways. If you mess up modification of the microservice such that it breaks other services that relied upon it, you quickly get into ceremony that a small team might struggle with. A monolith might have had the problem solved faster.
You're right of course, but it can also go the other way. If you break something then it seldom brings the service down, and often the breakage is very visible and can be seen by queues backing up. You can restore the broken service without having to redeploy the system as a whole.
Like most things in life there is no right answer, with tools like Terraform you can build a very complicated microservices system with not much effort, but only if someone on your team is experienced enough. If you're in a small team it's probably not worth the effort of learning the techniques and putting them in practice. We hate premature optimisation after all.
Quite possibly the first time I have agreed with everything in a post on HN! Just because a new paradigm exists doesn't mean it will solve all of your problems and will likely introduce several more!
Switching from Hadoop to Spark was clearly a good idea for our team, even though it required learning a new stack, but there isn't a strong reason to switch to Flink or start using Haskell.
Agile makes sense when your main risk is fine-grained details of user requirements, but not when you have other substantial risks, such as making sure a statistical algorithm is accurate enough.
Microservices probably reduces the asymptotic cost of scaling but add a huge constant factor.
Relational databases are the right choice 95% of the time, non-relational stores require a really specific use case.
TDD is good for fast feedback in some domains, but for others, manually investigating the output or putting your logic into types is better. E.g. a lot of my time comes from scaling jobs that work on 10gb of data but crash on 1tb, TDD is not that helpful here.
Continuous integration mostly makes sense when you're making a lot of small changes and can reliably expect a test suite to catch issues.
In short, ask the question "when is practice X useful?" instead of "is practice X a good idea?"