Design is about making choices.  When you do design, you narrow the design space down successively until you have found a space narrow enough that the recipient of the design can grasp it and make safe and effective use of it easily.

Whenever you make a choice—and remember, this is the essence of design—you're taking choice away from the recipient.  Hence, good design is about making choices that the recipient benefits from and will therefore not feel motivated to second-guess.  Bad design is anything that isn't good design.  You can't wiggle your way out of it: While there are many different degrees of goodness, refusing to do design at all is generally bad design.  If you leave a choice to the recipient that they don't care about, you haven't lived up to your responsibility as a designer, as you have made it harder for the recipient to use your product effectively, forcing them to make arbitrary choices that can only make things inconsistent and end up in disaster.  You have pushed the recipient into the Pit of Despair.


Quick: Which of the following two APIs for a string→int key--value store do you prefer, and why?

// ------- API #1 -------
class transaction {
  int get(string);
  void put(string, int);
  void del(string);
  void commit();
  //will abort when it goes out of scope unless you commit() first
};
class int_db_1 {
  unique_ptr<transaction> begin();
};
// ------- API #2 -------
class int_db_2 {
  void begin();
  void commit();
  void abort();
  int get(string);
  void put(string, int);
  void del(string);
};

I do not know about you, but even without considering the question of whether int_db_2 does or does not permit the use of get, put, and del outside of a transaction, it is obvious to me that int_db_1 is the better design.  Why?  Because int_db_2 is a pit-of-despair API.  It forces you deal with choices that you don't really care about, and which you will invariably get wrong at some point.  For example, you can easily fall into the trap of doing something like this:

void bump_access_count() {
  // Assume that db is declared in some kind of enclosing scope
  // (e.g. it is a field in the class that this is a method of).
  db.begin();
  int old_access_count = db.get("access_count");
  if (user_not_yet_seen(current_user)) {
    db.put("access_count", 1+old_access_count);
    remember_user(current_user);
  }
  db.commit();
}

Consider what happens when remember_user throws an exception.  Will this commit the transaction?  No.  (So far, all is well.)  Will it abort the transaction?  No.  So most likely, the DB object is now in a problematic state.  It might still hold a connection to some backend database service and hog resources that way.  It might fail to start a new transaction when you call begin the next time.  In any case, you have introduced a bug—and not only did you introduce a bug, you did it by being extra careful and using transactions (which is supposed to be the right thing)!  Because if you hadn't been using transactions in the first place, this wouldn't be a problem.  Maybe you'll avoid transactions next time.  And suddenly you'll get weird behavior because multiple processes are accessing the same database and seeing inconsistent data.  You have fallen into the Pit of Despair.

int_db_1, on the other hand, doesn't let any of this happen.  It won't let you think about whether to use transactions or not. It simply forces you to do so.  It won't let you neglect (or refuse) to handle exceptional cases. It simply forces you to either commit or abort (by aborting automatically if you don't commit).  There is no way to mess up accidentally.  And if the author of the transaction class has taken care to prevent copying and moving transaction objects, you can't even realistically cheat your way out.  Sure, you can make a wrapper and pass it around, or you can use low-level memory manipulation tricks to get at the internals of a transaction object and mess with them, but if you do any of that, you have explicitly taken responsibility for the disaster such a kluge might cause.  Surely, if you try, you can break it.  But you can't accidentally make a mess.  You have been pushed right into the Pit of Success.

This is what design is about.  When you do design, be it a UI or an API or anything else that requires making choices, make them.  Don't let the user make a mess.  If the user accidentally misuses your API, it's not because they are stupid.  It's because your design just isn't good enough.  Take the opportunity to learn from the user's mistakes and improve your design.  Not only will it make your users happy, it will also inform your future design.  A Pit of Success if ever there was one.