Sunday, November 22, 2020

Low-Code works best with a No-Code Model

One of the most common arguments against No-Code / Low-Code development is that software is just too complex to fit into a "database."  This perspective makes sense because there are definitely elements of code that do not fit comfortably into a database, however - the scaffolding and basic architecture of most systems certainly do.  Let me try to persuade you that writing code without a No-Code model is like walking a tightrope without a safety net!

The Argument 

  1. Hand Code (aka “source code”) isn’t going anywhere.

  2. Applications At Rest (i.e., in the DB) are usually great Single Sources of Truth.

  3. An SSoT is essentially a Cross-Platform Interface

  4. Defining Right and Wrong

  5. A quick example shows WHAT might go in a Specification Database.

  6. Production DB (at rest) vs. Specification Database

  7. Traditional Vs. Low Code User (developer) Experience

  8. Low Code Development Flow

  9. In Conclusion


1. Hand Code (aka “source code”) isn’t going anywhere

In the low-code model that I'm proposing, there is still totally 'source code" - though it's more accurate to call it Hand Code, I think.  I'm a developer - I want to write code.  I just don't want to write even a single line of code that a tool could write for me instead.  I want to write the actual function body in most cases.

Still, the context in which I'm writing the code, the names of things, the structure of parameters and return types, the purpose - at least for top-level behaviors, should be predetermined by the spec, and I simply have to provide the implementation.  And when I'm writing that implementation code, I should always have code completion for the vast majority of the structure for the system I'm operating within.  On any project, in any language, today, tomorrow, and in 5 years.  By the time I'm writing code by hand - I want my IDE to have most/all of the scaffolding needed such that I always have code completion.

All that said, if we're writing a function to convert text ToUpperCase - at some point, some code has to loop over the string and create and a new version of the String with uppercase letters.  I would never, not in a hundred years, attempt to put that actual logic or any such a function body into a database.  That is clearly something that belongs as source code. 

When first considering low-code technology, it is quite common for developers to quickly find several specific examples of “source code” that does not easily fit into a database.  Incorrectly though, this leads them to believe that it is valid to then dismiss the entire approach as being infeasible.


2. Applications At Rest (i.e., in the DB) are usually great Single Sources of Truth.

Yet think about the fact that we can stop most/all applications.  That they can somehow always come to rest, And then, when they restart, they can simply reload their state, usually from a database of some kind, regardless of how sophisticated or complex the application or underlying ideas are.  This fact seems to imply that virtually any concept, independent of its complexity, can be unambiguously represented in a well-defined, well-structured database.

In other words, when the system is turned off/at rest - many aspects of its behavior can be stored/saved to a database.  As a result, the specific size/shape of that database inevitably almost always confers a massive amount of context about what the system is, what it does, and what it is and is not capable of doing.


Even simple questions like the relationship of contacts with phone numbers may/may not be quickly answered by looking at the python or the Java code for the system.  But if you look at the database and see something called PhoneNumbers, with a single link/relationship to a specific Contact - then you know each contact can have N phone numbers.  

If, by contrast, the phone numbers entity has no link directly to any contact, then the relationships might be more complicated.  Suppose instead you find that there is a ContactPhoneNumber entity along with a CompanyPhoneNumber entity. In that case, we can probably infer that each Contact can have many phone numbers and that even multiple contacts and companies can potentially share the same phone numbers.  


All of this logic, the entities involved, their structure, their names, attributes, and relationships, usually provide a detailed context for understanding the limits of what the system can/can not do. Any system which allows you to export all of your data to (for example, a JSON file) will inevitably leak a massive amount of detail about their internal design/model.  Not because they accidentally made you a contributor in their repository but because they simply exported your data.


3. An SSoT is essentially a Cross-Platform Interface

What fits comfortably in a database is not the implementation - but rather the architecture.  What tends to fit comfortably are the names, attributes, and relationships between things the code is responsible for handling.  It's a little bit like an interface that crosses all of the languages in a given project.  It is a single source of truth - and is an essential ingredient needed to fill in an entire layer of code that is entirely absent from most “traditional” project repositories at this point. 


It's a question of where we choose to save all of the many decisions that we make when designing and implementing software - and many aspects of what the software does - just fits much more comfortably in a database than into "source code.”


So on one end of the spectrum, we do (and always will) have code that is and deserves to be written by hand.  That's hand code - and is one component of the "source code" - so in that way, we entirely agree.  Even those functions that we should be writing by hand, though, almost always belong to and exist as part of a more extensive system.  We really should define at least the actual size/shape of as much of that system as possible, outside of code. 


We honestly can pull the description of what that system needs to do apart from the details about how to do that in a specific context, and, as it turns out, the WHAT parts tend to be those that fit comfortably into a database, and the remaining “source code” then does fit comfortably in code. 

In a "traditional development" - the square “WHAT” and the round “HOW” get squeezed into one triangular hole called "SOURCE CODE" - and it is just a costly way to approach the problem.

4. Defining Right and Wrong
A Single Source of Truth, by contrast, which authoritatively defines the system’s behavior and is entirely absent in most "traditional" models, allows for many other benefits.  By far, the biggest is that it enables us to look at any part of the system and apply the label "RIGHT" or "WRONG" in a way that is almost always utterly unrealistic in most "traditional" environments. 

Specifically, we can label it "RIGHT" if it matches the single source of truth, and if it does not, we can mark it "WRONG.”  This process is simply not typically possible in a traditional model, at least in my experience, because it is not uncommon to have often considerable variation between the Specification and what gets implemented in the end.  Additionally, there is often variation between the "Back-end,” the API, the UI, the Documentation, and the Sales Materials within the final implementation.

Without rigorous process management and oversight - the “source code” is usually a complete hodgepodge of inconsistency between all these different layers.  So in a traditional model, there is simply is no "Right" and "Wrong.”  There is no right or wrong because the spec usually says one thing, but that gets changed during implementation and changed again when presented in the UI.  And we wrote the docs against a beta version, so they don’t match how the final version works - i.e., there's no "right" or "wrong,” there's just the way it works in each layer of the stack.



5. A Quick Example show WHAT might go in a SpecDB:

Picture a thought experiment where we are building a simple String Library.  We can decide that it will include a ToUpperCaase and a ToLowercase function - and describe what those functions will do in different situations - and what their parameters and return types will be - before we pick which language or languages in which to implement the library. 

We can define many of these parameters before we start writing a single line of code.  And in fact, once we pick a language - a simple tool can provide stub functions that just throw "not implemented" exceptions until we write the implementation code by hand.  Another conversion-tool can write simple unit tests that call each of those functions.


It's important to note, though, that if we choose to call the function ToUpperCase - we are going to want that function, with that name, to convert the input string To Upper Case in Python, and C# and Java, and JavaScript, Today, tomorrow, in 5 years in 100 years.  And if there's an entirely new language created in 5 years, and we port our library over to the brand new language - we're still going to want the function in that language to be called ToUpperCase, to take a string, and return a string. 


Maybe that language will instead call a String a tokenArray - but it will have an input TokenArray, and will likely also return a TokenArray. The general notion of what it should do will not change, though.  And we should define this description in a format that we can easily share between any language, any operating system, any technical environment/context. 

PHP is just not such an animal.  A JSON file, or XML file, or CSV file or a DBMS, a NoSQL Data Store,  or virtually any other easily other Queryable datastore - is a perfect place to put the structure and shared static contents of the system.  Not the implementation, the architecture.  The moving parts.  The actors.  The User Stories.  Shared Static Data. 


6. Production DB (at rest) vs. Specification Database

We should model the production system off of a non-production, single, shared, Specification Database - and any change made in “the system” should always start in the specification database.  If there's a new thing - we probably need an additional table to represent that thing.  If one or more entities have additional or different attributes, we probably need to add or change columns or relationships to other things.  Suppose there are new instances of things (states, categories, roles, activities, conditions, limits, configuration values). In that case, we probably need to add or modify rows to one of the specification database tables. Or even if the rules change about who can access what and when It should be possible to encode those details into the project specification DB.


One way or the other, though, any idea, however complex, whatever it's implementation languages or final operating environments will run in (iOS, Android, Web, Windows, Mac, Embedded System, Saas tool - whatever), is representable at rest.  In other words, the vast majority of the complexity of said systems can be represented, at rest, unambiguously in a well-defined database. And this can be designed thought out before we start to write any "code.”  A Single Source of Truth. 


We get unmatched cross-platform symmetry by sharing one machine-readable, easily queryable specification database across the N technologies in the project.  By doing this, the implementations across even expansive projects will all tend to all match each other in precisely the same way that N technologies in most "traditional" environments will tend not to match each other.  At least until they are each vigorously tested and validated.  Trying to achieve that symmetry “by hand” is just an extraordinarily costly way to do it - and it gets even more expensive as the project gets larger.  Thus with every additional "player,” whether it's a new language, a new system, or API, the complexity increases. 


7. Traditional Vs. Low Code User (developer) Experience

Specifically, the parts that will tend to fit comfortably in a database are the system’s parts, which will be true completely independently of which language or operating environment they are running in.  For example, the ToUpperCase function is part of a library that also includes a ToLowerCase method.  The existence of those two functions - and we can put a precise (English) description of what they will do into a database.  And we can add additional metadata about our string library in other rows.  In this way - before we've even decided whether this will be a JavaScript, C#, Python library - or maybe all three - we can enumerate the specific details of what the library will include. Perhaps we can split the list of 50 possible string functions into 3 phases/versions - the essential functions in version 1.  And then less critical parts in version v1.1 and v1.2. 


A simple report written against this metadata lists the three planned versions - and describes which specific functions to include in each version of the software.  


A simple, often reusable low code tool can convert that same metadata into, say, a Python module, with a template for each function that simply throws a "Not Implemented" exception.  The human developer then only has to write the actual "source code" for what happens when you call StringLib.ToUppercase("foo").  Or, what happens when the StringLib.ToLowerCase("FOO") is called.  Or - and this is actually the important bit - what happens when you call StringLib.DoFoo("abc").


8. Low Code Development Flow

When we add “SubString” to the list of supported functions in the SSoT, here's what a low code development context looks like - most of which would simply not be possible in a "traditional" development environment.


  1. 5 Unit Tests would immediately start failing

    1. Python StringLib.SubString(...) fails with error: "Not Implemented Exception."

    2. C# StringLib.SubString(...) fails with the error "NotImplementedException thrown by the target of the invocation."

    3. Javascript/TypeScript fails....

    4. PHP fails...

    5. Java fails...

  2. The next time the human developers log in on any of those platforms - they see their CI errors - and would now have, each in their own language, an empty "SubString(...)" function - which simply throws a not implemented exception.

  3. Once each developer writes their version of the function - and checks in that code, the UnitTest resolves itself.

  4. The documentation now shows four functions - at least for those languages which have passed the unit test.  The documentation can list those that do not yet pass as functions that are "Still in development.”

  5. Everywhere that used to previously mention the three functions that the string lib supports would now list 4.  

  6. We can link bug reports and feature improvements to the specific item in the specification database (the actual "source" of each of these functions) to get metrics, per function, of how they are doing.  

9. In Conclusion
The key is that the entire "system" is defined and managed outside of the code.  I.e., in a No-Code model. Ideally then, only the actual creative bits (how do we convert text from one form to another) need to be written by hand. 

For as much as possible of the entire system, though, the definition and planning details for the specific behaviors are all stored outside of the code.  With this abstract model in hand, low-code-tools can then create most/all of the infrastructure/plumbing/scaffolding along with much of the testing/documentation, i.e., the connective tissue - which is essential in virtually every system I’ve ever encountered.


In addressing your concerns about future-proofing, the key is that it is possible to add a 6th language at any time - and that new language can automatically start with placeholders for ToUpperCase, ToLowerCase, DoFoo, and SubString.  Additionally, there would immediately also be four failing unit tests - one for each of the functions which we have not yet implemented in that new language.


We can attempt to manage these problems with appropriate oversight and things like agile development methodologies. However, it is still a really expensive way to do it, like, an order of magnitude more costly than it needs to be.