Recent Posts (page 14 / 70)

by Leon Rosenshein

The Developer's Journey

I’ve been a developer for a long time now. I’ve seen others start down the path. I’ve seen people make mistakes and picked them up and helped them continue on their journey. I’ve seen their trials and tribulations. I’ve seen their highs and lows. And everyone’s journey is different. But there are some pretty common things I’ve seen along the way, especially amongst the most successfull.

Starting Out

In which the person sees an opportunity. It could be as simple as wanting to change the color of a texture or an operating parameter of an item in a game. It might be more complex, like automating some calculation in a spreadsheet, or it might even be some “advanced” thing like parsing some data out of a set of text files and then doing something with the data. But the developer rejects the opportunity and decides it’s easier to do it by hand or just live with things the way they are. After living with the problem for a while it turns out that someone the person knows has some experience and somehow, with a few deft keystrokes and function calls, solves the problem, almost magically. Armed with that new knowledge the person tries to do the same thing themselves.

Learning and Growing

Of course, doing it themselves is hard. At first it’s simply struggling with the language itself. What are the keywords? What kinds of loops and collections are there and which ones should be used when? Slowly it starts to make sense and they’re thinking less about how the language works and more about the problem they’re trying to solve. This kind of thinking expresses itself as levels of abstraction where the boundaries are between things. At first everything is ints, floats, and strings in a single giant method. As they progress in their ability and understanding they start to coalesce into types, libraries, and executables.

Systems, data, and their interactions start to be the driving factor instead of what a method or library does. They start talking about the problems in problem domain, not the code domain. Who took what? What’s the most efficient way to get that person from home to a hotel in a different city? How can we ensure that all of the food we need is made, everything we make is used, and leftovers are used to help those in need?

Sharing

At this point in their career, the developer is deep in the code. They understand it. They understand it’s place in the world. They know how a change in one place will have some unusual impact to something else. Not because of a problem, but because they understand the underlying connect between those two things. They’re usually very happy in that place. Often, they never leave.

But sometimes they do. Either they recognize themselves, or someone (a mentor or manager) points out to them that they could do more, for themselves and others, if they want to. They can teach others. They can help connect people and things. They can expand their scope and purview to include more and different things. They not only solve problems themselves; they help others solve the problems they have.

They recognize that being a developer is not just about code. It’s also (mostly?) about people, businesses, problems, and how they are all interrelated. Connection, influence, and passing on the learnings about how to avoid problems become the primary goals.

Or, as I’ve put it before, a developer’s career progression is about scope. Are you just looking at yourself, your team, your company, or your world. The broader your scope, the further you are in your journey.

Of course, I’m not the only one to look at the developer’s journey. According to Joseph Campbell, the developer’s journey goes through 17 distinct phases, often broken down into 3 acts. To wit:

  1. Departure
  2. Initiation
  3. Return

Wait a minute. That’s not the developer’s journey, that’s the Hero’s Journey.

This image outlines the basic path of the monomyth, or Hero's Journey. The diagram is loosely based on Campbell (1949) and (more directly?) on Christopher Vogler, 'A Practical Guide to Joseph Cambell’s The Hero with a Thousand Faces' (seven-page-memo 1985). Campbell's original diagram was labelled 'The adventure can be summarised in the following diagram:' and had the following items: Call to Adventure, Helper, Threshold of Adventure: Threshold crossing; Brother-battle; Dragon-battle; Dismemberment; Crucifixion; Abduction; Night-sea journey; Wonder journey; Whale's belly

They’re two different things, aren’t they? Consider about Bilbo Baggins. He didn’t ask to go There and Back Again. He didn’t even know that such a thing was possible when the story started. But he went. He learned. He understood. He had a wizard and good friends to help him along the way. Then he came home. And made the shire a better place. For himself and everyone else.

Maybe the developer’s journey and the hero’s journey aren’t that different after all. Something to think about.

by Leon Rosenshein

One Thing At A Time

Doing one thing and doing it well is The Unix Way. As I said in that article, tools should do one thing and do it well. If you need to do two things with two tools then connect them with a data pipe. It’s a great tenet for tools and applies to any number of systems that I’ve worked on. From text extraction to image processing to 3D model generation to an entire micro-service network.

It’s a great tenet in other areas as well. It lies at the heart of an agile development processes. Do one thing and finish it. See how it works. Get some feedback. Figure out what the next thing to do is. Do that. Lather, Rinse, Repeat. Take many more much smaller steps. You know where you are. You know where you want to be. The exact path between those two (and probably the exact destination) will change as you learn along the way. Uncovering better ways of developing software by doing it.

Another place it applies is change management. How you structure your checkins, code review (CR), pull request (PR), or whatever you call them. Every commit or CR should have one logical purpose. Sometimes that means that any given CR doesn’t add customer value. Sometimes, before you make a change that adds value you need to make a change (or multiple changes) that [makes the change you need to make easier)(/posts/2020/10/27/). And that’s OK. Because just like you should code for the maintainer, you should make CRs for the reviewer.

The question is, why is this important? After all, isn’t it faster to have one change, one big optimal step, that just works? In theory that might be the case. In practice it isn’t for, multiple reasons. Most importantly, ensuring that one big step ends up in the right place is probably impossible. We don’t know where that place is exactly, so there’s no way we can be sure we’ll hit it.

There’s another reason. A reason that has to do with combinatorics. Let’s say you have two changes you’re making. Let’s make it really simple by saying that the change either works or it doesn’t. Determining if it does or doesn’t is trivial. In this situation there are 4 possible outcomes. They both work, they both fail, or one works and the other fails. In this sitation. 75% of the possible outcomes are failures. Then. after you determine if the experiment is a failure you need to figure out which of the three possible failures it was. Then you need to fix it. The more things you try at once, the worse it gets. With 3 changes 88% of the outcomes are failures. Only the top path, with all results Heads (success) is a successful attempt.

With 4 changes 94% of the possible outcomes is a failure case. Any savings you get by taking that big step are going to be eaten up by dealing with all of the possible failures. You might get lucky once or twice, but over the long term you’re much better of makig one change at a time.

It doesn’t matter if the changes are in a single CR to be reviewed, a data processing pipeline, a microservice network, or the architecture of something you haven’t built yet. The more you change at once, the harder it is to know if the changes made things better or worse. So take the time to decompose your changes into atomic, testable, reversable steps and then make the changes, doing one thing at a time. You’ll be happier. Your customers will be happier.

And surprisingly, you’ll move faster as well.

by Leon Rosenshein

What's an Internal Customer?

I’m a platform and tool builder. I’ve spent most of my career building platforms and tools. Tools that others, inside and outside the company, use to do whatever it is they do to add value to their respective businesses. Even when the “tool” is something like Flight Simulator, built as a game to provide entertainment. Many, probably most, people who used Flight Sim used it as shipped. They might buy a airplane or some scenery, but basically they used it as shipped. But even those people also used it as a platform.

With platforms, I’ve talked about the difference between customers and partners before. It’s a big difference. With Flight Sim we had both. Some people who bought Flight Sim were clearly customers. They bought it, used it, and never talked to us about it. That’s a customer. Others, the people who built the add-ons, were partners. We worked with them. We made changes for them that made their jobs easier which made it possible for them to build more add-ons and make money doing it. Then, they built add-ons that we didn’t have time or resources to make. Those add-ons increased the demand for Flight Sim. So we did better. And the more copies of Flight Sim that sold the bigger the installed base they could sell to. So we treated our customers and partners differently.

And nowhere is the difference bigger than when you’re talking about the difference between an internal customer or partner. With Flight Sim our customers and partners were clearly external. With other platforms, such as the various versions of distributed processing platforms I’ve built, the customers were very much internal. We were building tools and platforms for other people in the company to do their work to build whatever product they were building. Sometimes it was maps, sometimes it was image processing. Sometimes it was large scale ETL jobs. Regardless of what they were doing, they needed our platform to do their job. So were they our customers or partners? They needed wht we were building, but if they didn’t need it, we didn’t need to build it. We needed each other.

Or at least we did at the beginning. As John Cutler put it, that internal team is your customer if you can

  1. walk away from the “deal”
  2. charge their “customers”
  3. sign contracts
  4. pursue work outside the company with other “customers”
  5. manage their own budgets
  6. hire their own teams

You know, the kinds of things you can do when you’re a company trying to sell something to someone outside the company. Of course, you can’t arbitrarily do any of those things without consequences, but there’s lots of choice on both sides. If that isn’t the case, for whatever structural, organizational, or financial reasons, it’s not a seller/customer relationship. It’s a partner relationship.

When we started building those platforms we had nothing to sell to our customer, and there was nothing they could “build/buy”, internally or externally. Neither side could walk away. We didn’t have individual budgets and we couldn’t just decide to go do something else. The situation was what it was, and we had to work with it. We had to work together to build the product(s) our customers wanted. We had to be partners in creating both the “product” our team was building and the maps/imagery/data sets that the other team needed. So that’s how we started out.

That doesn’t mean it had to stay that way. We aspired to have products that our customers wanted to buy. They aspired to have products to “buy” and that they could make feature requests on. And we eventually got there. By working together in partnership to build those first versions. And once we had products, as William Gibson said, the street finds its own uses for things. Once those other use cases were found, we could (bud didn’t) walk away from one of them because there were other customers. We could build a chargeback model. We had contracts (SLAs, usage commitments, etc.) We looked for (and found) other customers and related work. We got a budget and managed our own time and its size. In short, our partners had become customers.

That’s how you get from internal partners to internal customers. And give both sides the autonomy they want (need?) to get their jobs done and feel good about it.

by Leon Rosenshein

WIP and Queuing Theory

A distributed processing network with queues.

You never know where things will back up.

I’ve talked about [flow] a few times now. It’s a great state to be in and you can be very productive. On the other hand, having too much WIP inhibits flow and slows you down. And it slows you down by more than the context switching time (although that is a big issue itself). A common refrain I hear though goes something like “I need to be working on so many things at the same time otherwise I’m sitting around doing nothing while I wait for someone else.”

On the surface that seems like a reasonable concern. After all, isn’t it more efficient to be doing something rather than not doing anything? As they say, it depends. It depends on how you’re measuring efficiency. As an individual, if you don’t wait then you’re clearly busier. Your utilization is up, and if you think utilization is the same thing as efficiency then yes, the individual efficiency is higher.

On the other hand, if you look at how much is getting finished (not started), you’ll see that staying busy will reduce how much gets finished, not increase it. It’s because of queuing theory. Instead of waiting for someone to finish their part of a task before you get to your part you start something else. Then, when the other person finishes their part the work sits idle while you finish whatever thing you just started. Since the other person is waiting for you to do your part they start something else. Eventually you get to that shared thing and do your part. But now the other person is busy doing something new, so they don’t get to until they finish. So instead of you originally waiting for someone else to finish, the work ends up waiting. Waiting at each transition. The more transitions the more delay you’ve added to the elapsed time. Everyone can do eavery task in the optimum amount of time, but you’ve still added lots of delay by having the work sit itdle.

Explaining dynamic things with text is hard. Luckily there are other options. Like this video by Michel Grootjans where he shows a bunch of simulations of how limiting WIP (and swarming) can dramatically improve throughput and reduce cycle time. Check it out. I’ll wait.

What really stands out is that the queues that appear between each phase in a task’s timeline are what causes the delays. With 3 tasks there are 2 queues. In this case there’s only one bottleneck, so only one queue ever got very deep, but you can imagine what would happen if there were more phases/transitions. Whenever a downstream phase takes longer than its predecessor the queue will grow. If there’s no limit then it ends up with most of the work in it. Adding a WIP limit doesn’t appreciably change total time since the queue just lets the work sit there, but it does reduce the cycle time for a given task. It spends much less time in a queue.

And that cycle time is the real win. Unless you’ve done a perfect job up front of defining the tasks, limiting WIP gives you the opportunity to learn from the work you’ve done. In Michel’s example, if you learned you needed to make a UX change to something you could do it before you’ve finished the UX. You’d still have the UX person around and they could incorporate those learnings into future tasks. You’ve actually eliminated a bunch of rework by simply not doing the work until you know exactly what it is.

Of course, that was a simple simulation where each task of a given type takes, on average, the same amount of time. In reality there’s probably more variance on task length than shown. It also assumes the length of time doesn’t depend on which worker gets the task. Again, not quite correct, but things average out.

Even with those caveats, the two big learnings are very apparent. Limit WIP and share the work. Eliminate the queues and reduce specialization and bottlenecks. Everyone will be happier and you can release something better sooner. Without doing more work. And being able to stay in flow.

by Leon Rosenshein

Built-In Functionality

A pocket knife with multiple tools available.

You can use all the tools, not just the large blade.

Most languages have a way to start an external process. It’s usually called some version of exec, as in execute this processes for me please. There are generally lots of ways to call it. Synchronous and Asynchronous. Capturing the output,stdout and stderr. Passing arguments or not, or even piping data in viastdin. Capturing the exit code.

All those options are needed when you’re running external applications/executables. If you’re calling a 3rd party program to do some heavy lifting, you’ll probably want that level of control over what goes into the executable. You’ll want to know exactly what comes out, stdout, stderr, and any data persisted. If you need to then do something with the output data then you’ll want to wait for it to finish so you know it’s done and if it succeeded, so you’ll want to be synchronous. On the other hand, if it’s a best effort you might just want to know that it started successfully and have it keep running after you’re done. For all those reasons, and others, there are very good times and reasons to use the exec family of functions.

On the other hand, they’re also very easy to mis-use. In many (most?) languages it’s pretty trivial to run a shell command, pipe its output to a file, then read the file. If that’s all you do you’ve opened yourself up to a whole raft of potential issues.

The biggest is that if you’re exec’ing to a shell, like bash or zsh you never know what you’re going to get. You’re at the mercy of the version of the shell that’s deployed on the node/container you’re running in. You can hope that the version you want is in the place you want, but unless you’ve made sure it’s there yourself, you don’t know. Sure, you could write your shell script to use sh v1.0 and be pretty sure it will work, but that’s really going to limit you. The same goes with relying on standard unix tools in a distro. That works fine until someone sticks the thing you’ve written into a distroless container (or tries to build/run it on a Windows box) and suddenly things stop working. That’s why most languages have packages/modules/libraries built into them that provide the same kind of functionality you would get from those tools.

Second consider this little golang example. It’s much easier to just call

out, err := exec.Command("ls", "-l", "/tmp/mydir").Output()
fmt.Println(string(out))

than

 infos, err := os.ReadDir("/tmp/mydir")
 if err != nil {
  log.Fatal(err)
 }
 
 for _, info := range infos {
  entryType := "file"
  if info.IsDir() {
   entryType = "directory"
  }
  fmt.Printf("Found %s, which is a %s\n", info.Name(), entryType)
 }

and have the output right there on the screen. And that’s how it often done. But that easy leads to some big gaps where problems can sneak in. There’s no input validation or error checking. In Go at least you have to capture any error in err, but you never have to use it. And that snippet ignores stdout.

At the same time, you have to properly escape your input. With ls it’s not too bad, but you have to handle spaces, special characters, delimiters, and everything else your users might throw at you. Add in calling a shell script and it gets worse. The more interpreters between the thing you type and the thing that gets executed the more likely you are to miss escaping something so it gets to the next level as intended.

Finally, if you’re calling a shell script, how robust is it really? Code Golf might be a game, but it’s a lousy way to write reliable, resilient code. Even if the correct version of bash is used, and you get the argument parsing and escaping right, executing a script becomes an undebuggable, fragile, black box. And no one wants that.

So next time you think “I’ll just do a quick exec to get something done, think again. Use the tools of your language and check your work.

by Leon Rosenshein

Consensus vs. Consent

Consent and Consensus. Two very similar words. The first 6 letters are the same. The levenshtein distance is only 3. In general terms they both mean the same thing. If you have consensus you also have consent. The converse, however, is not true. In detail, they’re very different.

Consensus:

  • general agreement : UNANIMITY

Consent:

  • compliance in or approval of what is done or proposed by another : ACQUIESCENCE

It’s that last word in each definition that drives the difference. To get consent you need to make sure that no one is completely against the idea. That there’s no one who says, “You can do that, but you’re doing it without me. I will always argue against that action or point of view.” If you have consent everyone will go along with the decision. It might not be their first choice. It might not be the 10th. It might even be their last choice, but they’re OK with it. They will acquiesce to the decision.

Consensus on the other hand, means everyone thinks the plan/point of view is the best choice. No one has any doubts or thinks there might be a better way. Everyone is 100% on board and wondering why you haven’t started yet. This is a wonderful thing when it happens.

Think of it this way. For every idea/plan/proposal you have all of the people who get to weigh in get a vote. They can vote in one of 4 ways:

  • Yes: I think this is a great idea and we should do it now

  • OK: I’m willing to go along an support this idea. I don’t see any problems, so let’s do it.

  • No: I have a specific problem that needs to be addressed. Address my issue and I’m a Yes or at least OK

  • Absolutely Not: I completely refuse to be involved. I will not be part of a group the does this.

To get consent you need to get everyone to Yes or OK. If you have people in the “No” camp you need to address their concerns. You need to address their issue, but you don’t need to get them to think it’s the greatest idea every. Those in the “Absolutely Not” camp should be expected to provide an alternative. Since they think everything you’ve proposed is wrong, it’s on them to replace it all. In reality you’ll sometimes find someone who feels that way, but far more often, when someone says “Absolutely Not” they’re really just a “No”, with more emphasis. There’s a specific problem they see that they feel you’ve ignored. Address that issue and they become an OK. Getting everyone to “Yes” or “OK” can be hard. You’ll probably need to change the plan and there will be compromises, but it’s doable and when you’ve decided you have solid support behind you.

To get consensus, on the other hand, you need to get everyone into the “Yes” category. And that’s orders of magnitude harder. You have to get everyone to agree that the current idea is the best idea possible. That there’s no point thinking about it more.

Sometimes doing that is the right thing to do. If you’re on a road trip and you have time to make one stop for food you better make sure you have consensus. That everyone can get something to eat at the place you stop. If your group has 85% BBQ connoisseur, 15% omnivores, and one grain-free vegan (for medical reasons) you can’t stop at the BBQ joint that only serves brisket, pulled pork, buttermilk biscuits, and mac and cheese. It doesn’t matter how enthusiastic the BBQ experts are. The grain-free vegan can’t eat there. It’s not that they don’t want to or they’re being difficult. Eating there is physically bad for them and if they ate that food you’d be days late since they’d be in the hospital. You need to go the all-night diner down the road a little since everyone can get something. That’s consensus.

One the other hand, if that grain-free vegan says something like “I can’t eat at the BBQ place. It’s a physical impossibility. But there’s a market a couple of doors down. While you’re getting your food I’ll run over to the market and get something I can eat.” suddenly you’ve got consent. You can’t get consensus, but you’ve changed things so that you can get consent. And often, consent is all you need to move forward.

So next time you’re trying to build consensus make sure that’s really what you need. If you don’t need it and consent is enough, just go for that.

by Leon Rosenshein

Hey, That's Pretty Clever

A dungeon master with unruly hair and d20.
The Dungeon Master of Engineering has been on Twitter for just over 4 years now. There have been lots of snarky (but accurate) tweets about life as a developer. Recently there was a whole thread contrasting the viewpoint of someone new to tech with a tech veteran. Some are whimsical, some are political, and some are learnings about things developers deal with every day. There are lots of really good learnings in there when you look at them.

One of my favorites is

New to tech: 
That's really clever, ship it. 


Tech Veteran: 
That's really clever, fix it. 

I really like that one. Because I used to do clever things. Call things and rely on their side effects to save a few lines of code. Use Duff’s Device because it’s interesting and maybe faster even if the speed wasn’t needed, but Speed is Life. Or simple things like reuse a variable that wasn’t needed anymore to save a little stack space. Or in C++ use a , as a sequence point instead of just making a new statement.

Clever is nice. Clever is fun. Clever makes you feel smart. And we all like that. It’s great. Until it’s not.

Because the failure mode of clever is jerk. This is true when speaking or writing. Not just writing comments like tweets, but also when writing code.

Clever code often works at first. It might work for though a couple of requirement changes and refactors. And it might even work after that. But it’s value goes down fast. The code was written once. And it will be read many times. Now, every time someone needs to read the code to understand what it does, whether to extend it, refactor it, fix a bug, or just avoid adding a bug, that person will need to figure out what happens in the bit of clever code. That takes time. That takes effort. That increases cognitive load. Which makes everything harder.

And no one wants that. Software engineering, the balancing of conflicting goals and requirements to solve a user’s problems, is hard enough. There’s no good reason to make things harder on ourselves when we don’t have to.

I will acknowledge that sometimes you have to. If you’re writing an embedded controller and need to save every byte. If you’re working on the inner loop of a complex, time consuming rendering loop and your profiling has told you that this is the function that’s blowing your time budget. If you’ve found something new and novel in the domain that means your clever solution is actually the right one in this domain’s context. But those times are relatively rare.

So when you run across clever code, code with a slightly more verbose or slower implementation, code that can be written in a more maintainable way, consider fixing it. Make it less clever. You’ll be thanked by your peers and by future you. They’ll think you’re pretty smart for not subjecting them to clever code.

And that’s the best kind of thanks.

by Leon Rosenshein

Thinking Rocks, Magic, Intent, and TDD

A rock with eyes that thinks.

Can this rock really think?

A computer chip rendered useless after the magic smoke escaped.

Who let the magic blue smoke out?

Some have said that computers are just rocks we’ve taught to think. Others think computers run on magic blue smoke, and once you let the magic smoke out they’ll never work again. The truth, as usual, is somewhere between the two extremes. It’s not magic, and while arcing 120 VAC to ground across a chip will make a cloud of blue smoke and the chip will ever work again, it’s not magic. And no matter how many MFLOPS a chip can execute, it’s not really doing math. It just lets the electrons flow one way or another through a series of adjustable switches. From the outside though, it does seem like someone cast a spell on some tiny grains of sand (silicon) now the sand is doing math.

Whether it’s magic or good teaching, what does this have to do with Intent, let alone Test Driven Development? The connection is that intent is what drives both. The teaching was driven by the intent to build a machine that can do math quickly and reliably. Over and over again. And of course one of the primary rules of magic is that you have to keep the intent of the spell in mind when you cast it. Whether it’s Harry Potter’s “alohamora”, a djinn’s three wishes, or almost any other example of magic in the literature, it’s the intent behind the spell, not just the words, that defines what the spell operates on and how it works.

And it’s Intent that connects us to TDD. The intent of the tests in TDD is to express what should and should not happen. They’re an explicit expression of our intent for how the API should be used. They’re an explicit expression of what the limits and boundaries of the code are. They express what will work, what won’t, and how you know if it worked or not. And explicit is always better than implicit.

Leaving it implicitly expressed by the definition of the API and hoping users intuit your intent will only cause problems in the end. Hyrum’s Law tells us that, over time, anything users can do, they will do. That turns implicit requirements into explicit requirements as you work to avoid any breaking changes. Flight Simulator was like that. We needed to ensure all of the 3rd party tools and content worked, and with each new version it got a little more difficult to maintain compatibility with all those things that leaked through our interfaces.

Now you know how thinking rocks and the intent of magic are related to software development in general and TDD specifically. But magic has a lot more in common with development than that. After all, according to the literature, with magic, unless you follow the rules exactly things don’t always turn out the way you expected. At best nothing happens at all. At worst, something terrible happens. For more discussion of how the rules of magic also apply to software development, check out this thread from @bethcodes.

And beware the wily fae.

by Leon Rosenshein

Something Smells ... Primitive

I like types. I like typed languages. I find they prevent me from making some simple mistakes. The simplest example is that if you have something like int cookiesAvailableToSell you can’t do cookiesAvailableToSell = 2.5. You either have 2 or 3 cookies to sell. If you can sell the half cookie as a whole one you have 3. If you can’t then you have 2 cookies to sell and a little snack.

Picture of primitive tools
image source

I like domains and bounded contexts. They’re great at helping you keep separate things separate and related things together. Together, domains and bounded contexts help you stay flexible. They give you clear boundaries to work with so you know what not to mix. They make responding to business and operational changes easier by localizing contact points between components.

You’re probably wondering what types and domains have in common. It’s that a type is a domain. A byte (depending on language, obviously) is the set of all integers x such that -127 <= x <= 128. That’s a pretty specific domain. A character is also a domain. It’s very similar to a byte in that it takes up one byte, and can have a numeric value just like a byte, but it’s actually a very different domain, and represents a single character. They may have the same in-memory representation, but operationally they’re very different. If you try to add (+) an int and a char, in a typed language you’ll get some kind of error at compile time.

In an untyped language you never know what will happen. On the other hand, if you try to + a string and a char the result is generally the string with the character appended. That works because in the domain of text that makes sense. In the mixed domain of integers and text it doesn’t.

Which brings me to the code smell known as Primitive Obsession. It’s pretty straightforward. It’s using the primitive, built-in types in your typed language to represent a value in a specific domain. Using an int to represent a unique identifier. A string to represent a Universally Unique ID. Or a string to represent an email address. Or even an int to represent which one value of a defined (enumerated) set of values that something could possibly be. I’ve done all of those things. I’ve seen others do all of those things. And I’ve seen it work. So why not do it that way?

The most obvious is that you often end up with code duplication. Consider the case where there’s a string that represents an email address. Every public function that function takes an email address now needs to validate it. Hopefully there’s a method to do that, but even if there is you (actually all of the developers on the team) need to remember to call that method every time the user of the method passes in a string for the email. You also need to handle the failure mode of the string not being a valid email address, so that code gets duplicated as well.

Another problem is what happens if the domain of the thing you’re representing changes? You’ve got something represented with a byte, but now you need to handle a larger domain of values. Instead of changing the type in one place and possibly updating some constructors/factories, you’re now on a search for all of the places you used byte instead of int for this use case. And you’re looking not just in your code, but in all code that uses your code. That’s a long, complicated, error-prone search. And you probably won’t find all of them at first. Someone, somewhere, is using your code without your knowledge. Next time they do an update they’re going to find out that what they have doesn’t work anymore. And they’re going to find out the hard way.

Those are two very real problems. They make life harder on you and your customers/users. But they’re not, in my opinion, the most important reasons. There’s a much more important reason. Still thinking about that email address as a string, what if you have an API that sends an email. It’s going to need, at a minimum, the user name, domain, subject, and body. If you have all of them as type string then you make it easier for your user to get the order of the parameters wrong and not know until some kind of runtime error happens.

How else could it be done?

A better choice is to create a new type. A new type that is specific to your domain. That enforces the limits of your domain. That collects all of the logic that belongs to that domain into one bounded context. That abstracts the implementation of the domain away from the user and focuses on the functionality.

Sticking with the string/email, changing your APIs to take an email address instead of a string solves all of the issues above. Instead of getting an InvalidEmailAddress error from the SendEmail function the user gets an error when they try to create an email address. The problem is very localized. It’s a problem creating the address, not one of 12 possible errors when sending the email.

You never need to remember to check if the input string is a valid email address. You know it is when you get it because every email address created has been validated. Do the construction right and they can’t even send in an uninitialized email address.

If for some reason later you want/need to change from taking a single string to creating an email address from a username and domain you just do it. You can create a new constructor that does whatever you want with whatever validation you think is appropriate. All without impacting your users.

And best of all, this happens at compile time. Get the order of the parameters wrong and the types are wrong. A whole class of possible errors is avoided by ensuring it fails long before it gets deployed.

Because the best way to fix an error is to make sure it doesn’t happen in the first place.

by Leon Rosenshein

What Is Technical Debt Anyway?

Inigo Montoya saying Technical Debt. You keep using that word. I do not think it means what you think it means.

Technical debt has been on my mind a bunch the last few weeks. The system I’m working on has been around for a few years. It works, and it works successfully. However, since Day 1 we’ve learned a lot about what we want the system to do, what we don’t want it to do, and the environment it will be operating in. Some of those things fit into the original design, some didn’t.

According to Ward Cunnigham, who coined the term, technical debt is not building something you know is wrong, with the intent of fixing later. You always build things the best way you can, given what you know at the time. Technical Debt happens when you learn something new. Instead of refactoring the code to make it match the new knowledge you make the minimal change to the code to get the right answer, usually in the interest of time.

Two things to keep in mind here. First, when he coined the term, Ward was talking to financial analysts. People who were extremely familiar with the concept of debt and taking on debt to meet a short term need. They also understood the imperative of paying off that debt and the fact that if you didn’t pay off the debt you would eventually go bankrupt. They understood the context. That you can’t just keep increasing your debt and expect there to be no consequences.

Second, technical debt is NOT doing things badly, worse than you could, ignoring your principles and patterns, with the idea that you’ll do it right later. It’s not building a big ball of mud, without clearly separating your domains. It’s not hard-coding your strings everywhere because it’s easier or using exception handling for standard flow control. That’s just bad design and something that we should avoid.

Rather, Technical Debt is choosing to not refactor when you learn something new. You avoid going into “technical debt” by doing whatever refactoring is needed to ensure that that code models what you know about the system/domain. Doing anything else is considered tech debt. Once you have some tech debt you have to pay interest on it. That interest comes in the form of overhead, making it more difficult to make the next change when you learn something else. Eventually you end up in a situation where it’s almost impossible to make the change because the interest on the debt is so high.

There’s a nuance there that needs to be called out. Technical debt is not what happens when you do the wrong thing. It’s what happens when don’t do the right thing. It’s what happens when you’re doing the best you can, learn something new, and then don’t incorporate it.

There’s a time to take on debt. Just like a business, sometimes you take on debt to do something new. To open a store, take on a new line of merchandise, or just run a new advertising campaign. You take on the debt, see the benefit, then pay off the debt.

Whatever you do, don’t use technical debt as an excuse to do less than the best you know how to do.