3

There're certain things in code that can't be conditional. Two very common examples:

  • static types
  • dependencies.

You can't naturally enable/disable a feature that implies a new dependency or a new type with a condition (a feature flag).

How do the proponents of trunk-based development tackle such cases?

  1. package.json

    Suppose a new feature requires a new dependency. Now 2 versions have to somehow coexist in main and be controlled by some ENV var. DIY machinery around package.json and logically nested versioning around this file sounds very dangerous. You basically reimplement low-level VCS-like functionality for a single file.

  2. Types

    This can be mitigated to some degree in some languages but not in the others. For example in TypeScript, types can be dynamically inferred so it's possible to:

    export const foo = FEATURE_ENABLED ? _newFoo : _oldFoo
    export type Foo = typeof Foo
    

    Still it's pretty messy and does not answer how to approach the same problem in other statically typed languages.

6
  • 3
    Forwards compatibility? The existing features continue to just... not use the new dependency. And part of changing any existing type, whether for new features or just refactoring, is inevitably updating the things that already used it.
    – jonrsharpe
    Aug 10, 2022 at 11:50
  • 1
    You can't have >1 versions of the same dependency in the same package.json. End of story. So your reasoning is not applicable to Dependencies. As for Types, please check my reply below. Aug 10, 2022 at 12:22
  • 4
    You can't, but... so what? That would only matter in a situation where you needed a new version to support a new behaviour but the new version no longer supported an existing behaviour, at which point you'd have to change your plans anyway. This isn't really about trunk-based development or feature flags, just how you think about incrementally changing things.
    – jonrsharpe
    Aug 10, 2022 at 13:04
  • @IvanKleshnin: I am not familiar with packages in React, but typically the cases where packages cannot be installed in parallel in multiple versions are usually cases where the vendor guarantees backwards compatibility, otherwise it would be a bug. When there is such a bug, consider not to update the dependency unless the bug is fixed. To be fair, sometimes you want to to update to a new dependency and change your code so it deals somehow with some unwanted new behaviour. And that are indeed cases where feature flags are not the best tool for and where a new branch will suit you better.
    – Doc Brown
    Aug 10, 2022 at 13:51
  • 2
    A feature flag is somewhat similar to a VCS branch, and they should be avoided for similar reasons. It's preferable to use neither. But when we have to use one or the other a feature flag is generally the less bad option. See davefarley.net/?p=255
    – bdsl
    Aug 11, 2022 at 21:44

3 Answers 3

6

Feature flags should be used mainly for controlling the visible behaviour of a system from the outside. Introducing new types, or adding a dependency should not change that behaviour immediately, as long as the new type or code from the dependency are not used somewhere. And the usage itself can be controlled by a feature flag.

Modifications to existing types and dependencies can often be handled by keeping the old and new types in parallel in the code as long as the feature flag exists, or by keeping the old and new dependency both in the code, where this is possible.

Of course, when your software makes use of reflection to activate some new feature automatically as soon as a new type or dependency is available, then you need to work around this. But such mechanics is usually implemented dynamically and can hence be handled easily at run time.

However - in your comments, you asked: what about

  1. Introducing new versions of existing dependencies, where you cannot easily install two versions in parallel, and the new dependency is not fully backwards compatible?

  2. Introducing new versions of existing types, where maintaining these two versions in parallel causes some cascading effekt throughout a huge part the code base?

For those cases I think feature flags are indeed not the ideal tool. Separate branches give you the chance to develop and test some code to deal with some incompatiblities first in isolation, which gives you a lower risk of breaking anything before merging it back into the trunk. The disadvantage is that you may have more work to resolve merge conflicts. Hence it is sometimes good to have two different tools in your toolbox and know when to choose which.

Let me add I have a lot of first-hand experience with both approaches. Where I work, we handle at least 95% of our feature development with feature flags, and maybe 5% or less by feature branches. But YMMV.

4
  • Thank you. It's more or less clear when we add things but let's focus on changing the existing code which happens even more often, at least in large projects. Say I need to modify a prop of a React component which is already present on some page. Some TS types will be changed in process. Should I copy the whole implementation to ComponentV2 and make its use conditional?! The key downside of such approach is a lot of extra ADDED and REMOVED lines in VCS. To change a single prop I can easily end up duplicating a 1000 LOC component => 2000+LOC diff. Aug 10, 2022 at 12:19
  • @IvanKleshnin you can generally make the new type be a union of the type the old code needs and the type the new code needs. Or add a new prop instead of modifying the existing prop.
    – bdsl
    Aug 10, 2022 at 13:23
  • 1
    Yes. Same principles as with database migrations. A lot of ceremony to resolve a seemingly basic problem like prop renaming. I confess I often just skip F.Flags in such cases and rename the thing relying on tests or code review to spot errors. Aug 10, 2022 at 18:34
  • 2
    @IvanKleshnin: this is why Doc Brown also mentions using branches in version control. When you need to start doing some really hairy things with imports and feature flags, you are using the wrong tool. Branch the code in your VCS. Aug 11, 2022 at 2:05
1

I am more backend-oriented but what we do with Java is just update the the library and static objects in a compatible way.

Example:

  • Update a core library: just add the new version. If this is backward compatible there is no need to do anything special. Let's say that it is a new incompatible version. Now you will need to create a commit that updates any changes to make it compatible. Your test suite should be able to detect any issue in local. If not, you need to consider if the project desired level of availability allows you to use trunk based development.

  • Update an static type: I am not 100% sure of what do you refer but lets assume that you are adding a new field to the DTO -> Domain -> Database. You can add the new field to the DTO as optional without breaking the contract. You can update the database and create it as nullable with null as default value. You will need to update both database in a backward compatible way. Once that you update the code it may return the new field if stated and this should be backward compatible. On the long run you can check that all values are filled you can change to a non-null value. For more drastic changes you may need to a new version of the endpoint.

Remember that trunk based development requires good observability to detect issues in production and the ability to rollback fast to a previous version. Some deployment patterns like canaries, dark deployments and similar may help you to be safer.

2
  • 1
    Thank you. That's similar to what we do ourselves and my point is that there WILL be commits that won't follow "hidden under a feature flag" principle. They will be just merged (or rejected) and safety will rely on static types, tests, manual review, etc. Some changes are just not togglable by nature and you have either one or another version in the code but not both. The problem is that authoritative resources like trunkbaseddevelopment.com never mention this point. Aug 10, 2022 at 18:21
  • 1
    That is right @IvanKleshnin. Making all changes "toggeable" is madness. It will require a lot of effort or even become impossible. Feature Toggles exist to allow pushing to production without activating the feature. So all backward compatible refactors do not need feature toggle by definition. Imagine upgrading the React version. If everything works... just push the new versions with the updated changes. There shouldn't be any functional behaviour changes so you do not need to coordinate with business.
    – Borjab
    Aug 10, 2022 at 18:41
1

Excellent points re the limitations of feature flags. However, I think there is limited scope for applying this criticism to trunk based development.

If you think about it, these kind of changes are a problem however you do em. A branch will cause merge conflicts, shared dependencies cant be rolled back etc

Trunk based development ignores the problems that branching is meant to solve. ie multiple features being worked on at the same time independently.

Trunk based says. instead of that, lets check in small changes so fast that we are all effectively working on the same branch. We don't merge one feature first and then another, we don't roll back, we don't have releases.

If you are always going forward then the kind of changes you are talking about don't pose a problem. the extra enum value is hidden, the extra dependency is there for everyone.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.