jammycakes
The cases where I've experienced problems with feature toggles have been where we thought we were swapping out all the functionality but it later turned out that due to some subtleties or nuances with the system that we weren't familiar with, we had overlooked something or other.
Feature toggles sound like a less painful way of managing changes, but you really need to have a disciplined team, a well architected codebase, comprehensive test coverage and a solid switching infrastructure to avoid getting into trouble with them. My personal recommendation is to ask the question, "What would be the damage that would happen if this feature were switched on prematurely?" and if it's not a risk you're prepared to take, that's when to move to a separate branch.
I've worked with both feature branches and feature toggles, and while long lived feature branches can be painful to work with what with all the conflicts, they do have the advantage that problems tend to be uncovered and resolved in development before they hit production.
When feature toggles go wrong, on the other hand, they go wrong in production -- sometimes, as was the case here, with catastrophic results. I've always been nervous about the fact that feature toggles and trunk based development means merging code into main that you know for a fact to be buggy, immature, insufficiently tested and in some cases knowingly broken. If the feature toggles themselves are buggy and don't cleanly separate out your production code from your development code, you're asking for trouble.
This particular case had an additional problem: they were repurposing an existing feature toggle for something else. That's just asking for trouble.
It's certainly far, far better than having to add exactly the same check after every method call. Which is only what you need to do if you're working in a situation where exceptions are not an option.
var myFile = File.Open(filename);
try {
while ((var s = file.ReadLine()) != null) {
var entity = ProcessLine(s);
// do whatever you need to do to entity
}
}
finally {
myFile.Dispose();
}
C# gives you the using keyword as syntactic sugar for this: using (var myFile = File.Open(filename)) {
while ((var s = file.ReadLine()) != null) {
ProcessLine(s);
// do whatever you need to do to entity
}
}You can only assume that the methods you have called have left their own work in a consistent state despite having thrown an exception. If they haven't, then they themselves have bugs and the appropriate cleanup code needs to be added there. Or, if it's a third party library, you should file a bug report or pull request with their maintainers.
You don't try to clean up other people's work for them. That would just cause confusion and result in messy, tightly coupled code that is hard to understand and reason about.
Not necessarily. You should only handle expected conditions locally if there is a specific action that you need to take in response to them -- for example, correcting the condition that caused the error, retrying, falling back to an alternative, or cleaning up before reporting failure. Even if you do know what all the different failure modes are, you will only need to do this in a minority of cases, and those will be determined by your user stories, your acceptance criteria, your business priorities and your budgetary constraints. That is what I mean by "expected conditions." Ones that are (or that in theory could be) called out on your Jira tickets or your specification documents.
For anything else, the correct course of action is to assume that your own method is not able to fulfil its contract and to report that particular fact to its caller. Which is what "yeeting exceptions up the call stack" actually does.
> Almost everything else you listed represents a bug in your software that should terminate execution.
Well of course it represents a bug in your software, but you most certainly do not terminate execution altogether. You perform any cleanup that may be necessary, you record an event in your error log, and you show a generic error message to whoever needs to know about it, whether that be the end user or your support team.
Again, what action you need to do in these cases will depend on your user stories, your acceptance criteria, your business priorities and your budgetary constraints. But it is usually done right at the top level of your code in a single location. That is why "yeeting exceptions up the call stack" is appropriate for these cases.
You only terminate execution altogether if your process is so deeply diseased that for it to continue would cause even more damage. For example, memory corruption or failures of safety-critical systems.
> I’m more than a little shocked that you think yeeting exceptions up the call stack is appropriate for these cases.
I hope I've clarified what "yeeting exceptions up the call stack" actually does.
The alternative to "yeeting exceptions up the call stack" when you don't have any specific cleanup or corrective action that you can do is to continue execution regardless. This is almost never the correct thing to do as it means your code is running under assumptions that are incorrect. And that is a recipe for data corruption and all sorts of other nasties.
That's all very well as long as people actually do that. It doesn't always happen in practice.
> And yes, “railway oriented approaches” can absolutely do this.
How? Please provide a code sample to demonstrate how you would do so.
Unfortunately, you can only do that when the number of possible failure modes is fairly limited. In a complex codebase with lots of different layers, lots of different third party components, and lots of different abstractions and adapters, it can quickly become pretty unwieldy. And then you end up with someone or other deciding to take the easy way out and declaring their method as "throws Exception" which kind of defeats the purpose.
Probably not so much of an issue if you're dealing with well validated CAD data and most of your processing is in-memory using your own code. But if you're working with enterprise applications talking to each other via microservices written by different teams with different levels of competence, legacy code (sometimes spanning back decades), complex and poorly documented third party libraries and frameworks, design decisions that are more political than technical, and so on and so forth, it can quickly mount up.
Figuring out, at compile time, what sort of exceptions appear inside a given function is a futile exercise in many contexts, and railway oriented programming does not fix it. Java tried this with checked exceptions and it fell out of favour because it became too unwieldy to manage properly.
In any significantly complex codebase, the number of possible failure modes can be significant, many of them are ones that you do not anticipate, and of those that you can anticipate, many of them are ones that you cannot meaningfully handle there and then on the spot. In these cases, the only thing that you can reasonably do is propagate the error condition up the call stack, performing any cleanup necessary on the way out.
"Handling this invisibly" is also known as "convention over configuration." In languages that use exceptions, everyone understands that this is what is going on and adjusts their assumptions accordingly.
https://fsharpforfunandprofit.com/posts/against-railway-orie...
Railway-oriented programming is an interesting concept and it does have its use cases, but it does need to come with a massive health warning. I've often seen it used in practice to reinvent exception handling badly, and this is something I consider particularly ill advised because exceptions, when understood and used correctly, provide a much cleaner and more effective way of handling error conditions in most cases.
The thing about exceptions is that in most cases, they make the safe option the default. An error condition is an indication that your code can not do what its specification says that it does, and in that case you need to stop what you are doing, because to continue regardless means that your code will be operating under assumptions that are incorrect, potentially corrupting data. Error conditions can happen for a wide variety of reasons, many of which you do not anticipate and can not plan for, and in those cases the only safe option is to clean up if necessary and then propagate the error up to the caller. Exceptions do this automatically for you by default (you need to explicitly override it with a try/catch block) but alternative approaches, such as railway oriented programming, require you to add in a whole lot of extra boilerplate code that is easy to forget and easy to get wrong. If you can't handle the error condition on the way up the call stack, you would then log it at the top level and report a generic error to the user.
Having said that I see two particular use cases for this kind of technique. The first is situations where you need to handle specific, well defined and anticipated errors right at the point at which they occur. Validation is one example that comes to mind; another example is where you are trying to fetch a file or database record that does not exist. The second is situations where exception handling is not available for whatever reason. Asynchronous code using promises (for example with jQuery) are pretty much an exact implementation of railway oriented programming, but since modern JavaScript now has async/await, we can now use exception handling in these scenarios.
I think that's how DevOps is actually supposed to be done in the first place. You view Ops -- and the code used to manage and support it -- as a product, and get a specialised team of experienced Devs (and architects) to build it.
Once you've got the basic infrastructure and architecture in place, you then train up the individual development teams to customise it, extend it and troubleshoot it as they need to. In much the same way as they do with any other software product.
It started out well -- we spent a few months hacking with Terraform, Docker, Vagrant, Kubernetes, and related technologies to implement an infrastructure-as-code approach -- automating the process of provisioning and decommissioning servers, and building a setup where development teams could deploy updates with a simple git push.
Unfortunately it all went downhill fairly rapidly. We ended up spending the majority of our time manually applying security patches to a bunch of snowflake servers that we'd lifted-and-shifted from another hosting provider to AWS, and fielding support requests from the development teams. Within a year, we were being told in no uncertain terms by our project manager that we were an operations team, not a development team.
It felt like a complete bait-and-switch. Within two years, I had left the organisation in question and moved on to a new job elsewhere doing actual development again. Last I heard, the entire team had been disbanded.
It sounds like the author of this article must have had a very similar experience. I wonder just how common it is. It seems that in many places, "DevOps" is all Ops and no Dev.
The point that I make about patterns and practices in programming is that they need to justify their existence in terms of value that they provide to the end user, to the customer, or to the business. If they can't provide clear evidence that they actually provide those benefits, or if they only provide benefits that the business isn't asking for, then they're just wasting time and money.
One example that Uncle Bob Martin hammers home a lot is separation of concerns. Separation of concerns can make your code a lot easier to read and maintain if it's done right -- unit testing is one good example here. But when it ceases to be a means to an end and becomes an end in itself, or when it tries to solve problems that the business isn't asking for, it degenerates into speculative generality. That's why you'll find project after project after project after project after project with cumbersome and obstructive data access layers just because you "might" want to swap out your database for some unknown mystery alternative some day.
The beauty of exceptions, on the other hand, is that the default option is the safe one. Sure, forgetting to add error handling may leave you presenting a user with a stack trace, but at least you're not billing them for something that never gets delivered.
As other commenters have noted, this post is seven years old. My position on feature branches has evolved (and softened somewhat as well) in the meantime.
I posted an update about 18 months ago:
https://jamesmckay.net/2017/01/martin-fowler-and-feature-bra...
Incidentally I wrote a whole series of blog posts a while ago where I cast a critical eye over the whole n-tier/3-layer architecture and explained why it isn't all that it's made out to be.
The fact is that sometimes you have to ignore the "rules," because the "rules" were designed to serve a purpose that does not apply in your particular case, or perhaps never even applied at all in the first place.
The problem with trying to separate your business layer from your data access layer is that it's often difficult if not impossible to identify which concerns go into which layer. Take paging and sorting for example. If you treat that as a business concern, you end up with your database returning more data than necessary, and your business layer ends up doing work that could have been handled far more efficiently by the database itself. On the other hand, if you treat it as a data access concern, you end up being unable to test it without hitting the database.
You need to realise that software development always involves trade-offs. Blindly sticking to the "rules" is cargo cult, and it never achieves the end results that it is supposed to.
Nice idea in theory, sometimes impossible in practice.
A few years ago I came onto a project that had a very clearly defined separation of concerns, with a business layer, data access layer, presentation layer, and Entity Framework. This was resulting in a number of SQL queries that took over a minute to run and caused web pages to time out.
I ended up cutting right through the layers, bypassing Entity Framework altogether and replacing it with hand-crafted SQL. This ended up cutting down the query time from six minutes to three seconds.
All employers are looking for is some publicly available evidence that you actually have the skills that you claim to have listed on your CV.
The way I write it is similar to test-driven development — I write down each step in what I'm doing before I do it. When I run a command, I copy the command itself into my notes, then once it's run I copy the important parts of the output (e.g. any error messages).
It really comes into its own when I want to pick up on a task that I had put to one side a few days previously, or when I run into a problem that I think I'd encountered before. It also makes it much easier to avoid the trap of trying the same thing several times before you realise that you're going round and round in circles.
It's also useful for writing documentation. You can just copy and paste what you've written in your lab notes into your commit summaries, Jira tickets, e-mails, spec, help files, whatever, then tidy up as appropriate.
Also, I'd have thought that with weekend projects you're more likely to find things out by experimentation and reading the documentation than by asking questions. With work or classroom projects, you have to work with a fixed spec, a prescribed technology, all sorts of Best Practices, and deadlines. Weekend projects give you much more freedom in all four respects.
So yes, GitHub would be much more accurate.
I've found that in the absence of a hard limit, you all too quickly end up with a codebase riddled with 250+ line monsters forcing you to scroll right and left all over the place just to get the faintest smidgen of an idea of what's going on. It's a maintenance nightmare.
Meanwhile, GitHub won't show lines longer than 132 characters (on Windows) or 123 characters (on the Mac) without scrolling, no matter how wide your screen.
I personally find 100 seems to strike the right balance.
The annotations also allow the development of better IntelliSense-like tools.
Things that Subversion can't do
But my whole point is that there's far too much scope to get it wrong in ways that can be difficult to fix.
If you have multiple projects in your repo, svn cp and svn switch become cumbersome.
Many people check out the root folder with the entire structure and all the branches, and not just trunk.
I've seen projects that have nested branches/tags/trunk structures.
I've seen people check code in to two branches at once.
I've seen people "branch" by physically copying the files client-side then checking in the result. Losing the relationship between the branches that they need to merge successfully.
If you check in the merge then find you've messed it up, you can't roll back and start over without cluttering up your source history. And even then, how to do that is non-obvious and easy to get wrong.
And people DO make these mistakes repeatedly because svn treats branching and merging as an advanced technique. The first several times you do it, you invariably mess up, and leave a permanent record of how you messed up. Very often it's not you who learns that you've messed up but a colleague who does the merge. And once you learn to get it right, someone else on your team comes along and they get it wrong. The feedback loop is terrible.
Whereas with git it's a core competency so you learn you've messed up fairly quickly. And offline commits let you easily roll back when you do. The feedback loop is rapid and effective.
Totally misses the point that the expense of branching and merging in Subversion is all in the UI/UX. Not only is it horribly cumbersome, it's also much easier to get it wrong, not obvious that you are getting it wrong, and harder to recover once you've discovered you've got it wrong.
Shelving and checkpointing have been planned for Subversion for several years now and though they're currently slated for version 1.10, currently in development, work on those features hasn't started yet. Given that it's been bounced from 1.9 already, and that Subversion release cycles are measured in geologic eras, I'm not holding my breath.