Gall’s Law

Today I want to discuss Gall’s Law, which was named after an author and paediatrician called John Gall. In 1975 he wrote a book called General Systematics, an essay on how systems work and especially how they fail. It’s from his insights in this book that Gall’s law was coined.

Gall’s law states

A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.

We see the effects of this all the time in software development. How many times has a product team decided that the system they’re currently working on is rubbish and the best thing to do is start again.

Then, when we start again, it’s never as good as it was before, it takes longer than expected, and there is challenge after challenge.

The whole purpose of the lean software development methodology was, on reflection, all about mitigating the effects of Gall’s law.

Start simple, get a system that works to solve the main use case, test and learn and then evolve it to match your customer’s needs. We know that iteration is the best way [00:01:00] to ensure that you create a software product, create a system that works for your business and your customers.

It’s worth us all keeping that in mind, especially within the context of accelerating digital delivery. You can’t deliver well and deliver fast if the premise that you are building on is wrong.

So if the premise is that we will build this new thing because it will be better than the old thing and Gall’s law is correct, then we know that’s already a non-starter.

So next time you’re frustrated with the system you’re working on. Remember Gall’s law about how successful systems evolve.

Create small features and layer on top of them to create good, reliable software that meets the business and customer needs.

83: Carbon capture with seaweed.

An amazing guy, Howard Gunstock, who I worked with a Three started a business called Carbon Kapture around four years ago.

He was driven to do something about the upcoming climate catastrophe and worked hard to build an eco-brand focusing on carbon capture. Yesterday, he launched and awesome ‘sponsor a rope scheme’.

For just £20 you can sponsor a meter of rope of seaweed which will capture around 1.8kg of CO2,

As part of the package Howard and the team will give you updates on how it’s growing and where it’s eventually used in farming to improve the quality of the soil.

Carbon Kapture hopes to have 1 million metres of seaweed ropes in the water by the end of 2024. This is impressive work. Especially as Seaweed absorbs CO2 at 30x the speed of trees.

If you care about the environment, perhaps you’ve bought bamboo kitchen implements because they’re more sustainable, then this will be right up your street.

82: MS Graph API – Adding Users to a group (python)

Following up on my posts on:

Here is another simple script for adding users to an Azure Active Directory (AAD) Group.

def AddUserToGroup(user, group_id):
        Adds a user to an AAD group 
        User = User object from GetUserByEmail()
        group_id = AAD group id
    headers = {"AUTHORIZATION": f"Bearer {token}", "Content-type": "application/json"}
    url = f"{group_id}/members/$ref"
    data = {
        "": f"{user['id']}"
    response =, headers=headers, json=data)
    if response.status_code == 204 :"User {user['displayName']} added to group: {response.status_code}")
    elif response.status_code == 400 :
        logger.error(f"ERROR: User {user['displayName']} already exist in the group: {response.status_code}")
    elif response.status_code == 404 :
        logger.error(f"ERROR: User {user['displayName']} or group not found: {response.status_code}")

I’m passing the user object received from the GetUserByEmail method, previously shared and the AAD Group ID defined in an environment variable.

Return codes

204 – The Users has been successfully added to the group
400 – The Users already exists in the group
404 – The user or group cannot be found.

This method adds asignle user, but the endpoint supports batch uploading via a PATCH Request

Content-type: application/json

  "members@odata.bind": [

81: MS Graph API – Remove a User from a Group (Python)

Following on from yesterday’s post on Finding a user by email, here’s a method for removing a user from an AAD group using MS Graph API.

def RemoveUserFromGroup(user, group_id):
    headers = {"AUTHORIZATION": f"Bearer {token}", "Content-type": "application/json"}
    url = f"{group_id}/members/{user['id']}/$ref"
    response = requests.delete(url=url, headers=headers, json=data)
    if response.status_code == 204 :"User {user['displayName']} removed to group: {response.status_code}")
    elif response.status_code == 200 :
        logger.error(f"ERROR: User {user['displayName']} does not exist in the group: {response.status_code}")
        logger.error(f"ERROR: User {user['displayName']} cannot be removed: {response.status_code}")

I’m passing in the full user object received from the GetUserByEmail method.

We use the User’s Id property in the /groups/ endpoint to remove the user from the Group ID.

If the user is removed, the endpoint will return a status code of 204.

If the user isn’t in the group and can’t be removed, the endpoint will return a status code of 200.

80: MS Graph API – Find a user by email (python)

A simple method for finding a user by email via Graph API

def GetUserByEmail(email):
    headers = {"AUTHORIZATION": f"Bearer {token}", "Content-type": "application/json"}
    url = f"$filter=mail eq '{email}'"
    response = requests.get(url=url, headers=headers)
    if response.status_code == 200 :
        item = response.json()
        if len(item["value"]) != 0 :
            _user = response.json()["value"][0]
            return _user
            logger.error(f"ERROR: User {email} not found: {response.status_code}")
        return None
    else :
        logger.error(f"Error: User {email} is borked: {response.json()}")
        return None

In this code fragment, we hit the user API with a filter for the user’s email address.

The endpoint always returns an object regardless, so we need to check for the length of the Value array.

If this is empty we return a null value.

You may also like:

66: Python: MS Graph API Authentication (Without a user)

81: MS Graph API – Remove a User from a Group (Python)

70: Obsidian

I finally got my shit together and migrated my notes from Apple Notes to Obsidian, it turned out to be less painful than I was expecting, BUT it’s a FAF.

For over a decade, I’ve chosen to write all my documents using a Markdown editor of some variety: IAWriter, Bear and now Obsidian because I want access to my files in a format that won’t become obsolete. Markdown is perfect because when all’s said and done, it’s just a text file.

There is one notable exception to this rule, Apple Notes! Notes are so easy to use that I find myself using them by default; I just couldn’t get out of the habit.

There are two main stumbling blocks

1) It’s on all my devices by default
2) It’s where all my notes live

The device problem is pretty simple to overcome; I installed obsidian on all my devices. Stumbling block two has hindered me, though. Getting your notes out of Apple Notes is next to impossible. To do it, I needed to dedicate time, and it just wasn’t high enough on my list of priorities.

That changed this week, though, because I finally connected Obsidian to Readwise to import all my notes highlights from Kindle, Instapaper, Twitter and Apple Books, which I’ve also meant to do for a long time.

This morning I decided to put some time aside and get it done. Here’s a link to the migration guide I used As I already have bear, I decided to go down the following route:

  1. Download Bear’s automation workflow
  2. Use it to export all notes to HTML pages
  3. import the HTML pages into Bear
  4. Export the HTML pages as Markdown files
  5. Copy the Markdown files into Obsidian.

There are some limitations to this approach as noted in the Bear documentation

- Text, lists, and photos should be included

- **Note** : only macOS 10.15 Catalina supports photo export. On earlier macOS versions, Apple Notes will not export photos

- Task lists convert into bulleted lists - Rich media links will convert to plain text links

- Non-photo attachments like PDFs or other files **are not supported** and will be excluded from export to HTML files. They will remain safely in Apple Notes

All in all, it took around 10 mins to migrate the notes, import, export and import again, but it’s done. Now, hopefully, I can use Obsidian for all my knowledge capture.

69: See ya January


I’ve been super busy these past few weeks, and sadly, my writing has taken a back seat, but I wanted to end the month with a recap of what we achieved.

On 3/1/2023, I wrote in 50: Back to work tomorrow that we wanted to achieve the following things:

  • 1. Finish the Retail App
  •  2. Complete the migration project
  •  3. Complete the Design a better world website
  •  4. Completed The Alice Sound Website

So, how did we do?


  1. Finish the Retail App > we pushed the app live on the 24th Jan (68: Retail App), and tomorrow on the 1st Feb it will be comms out to 2500 people in the Retail teams at Three.
  2.  Complete the Design a better world website > we are code complete and have handed over the site for Don and Juna to start populating with content to support the publication of his new book.

Partial Success

  1. Complete the migration project > this project has been challenging, but today we finalised the migration and reorganisation of the first Sharepoint site. The project is not complete yet, but we have a script that will let us map the old folder locations to new ones and move the files. I’ve been posting the odd snippet about this  (68: Generative AI you need to check its working outs67: JavaScript templates strings in Python66: Python: MS Graph API Authentication (Without a user)).When I get the chance, I’ll post the script, as it might be helpful to people who need to create and move folders in SharePoint using GraphAPI.
  2.  Completed The Alice Sound Website > this has been delayed. We’re done with our bit for now, but we look forward to working with the team to finalise the site and release it to the world.

Aside from these goals, the team, led by Dan, have delivered lots of work for Etch, helping them with several clients, and we mustn’t forget the work we’ve been doing with Accenture to support the delivery of the Strata Application.

I’m very proud of the work our team has completed through Jan.

68: Generative AI you need to check its working outs


I’ve mentioned a couple of times that I’ve been playing with ChatGTP to assist with a piece of development work I’m doing.

Today, I validated a new piece of code it presented me with, which looked great and was exactly what I wanted but completely wrong.

I asked it

How do I move a SharePoint folder using Microsoft Graph API in python?

It replied (initially with the correct answer)

import json
import requests

folder_id = "123"
new_parent_id = "456"

url = f"{folder_id}"
headers = {
    "Authorization": "Bearer your_access_token",
    "Content-Type": "application/json"
data = {
    "parentReference": {
        "id": new_parent_id

response = requests.patch(url, headers=headers, json=data)

if response.status_code == 200:
    print("Folder moved successfully.")
    print(f"Error moving folder: {response.json()}")

Then I asked Can I move a folder using a path rather than IDs

Where it replied as follows

Yes, it is possible to move a folder in SharePoint using a path rather than IDs using the Microsoft Graph API.

You can use the Move API to move the folder by passing the path of the folder and the new parent folder.

Here is an example of how you can use the requests library to move a folder located at “path/to/folder” to a new parent folder located at “path/to/newparentfolder” :

and supplied the following code

import json
import requests

folder_path = "path/to/folder"
new_parent_path = "path/to/newparentfolder"

url = f"{folder_path}:/move"
headers = {
    "Authorization": "Bearer your_access_token",
    "Content-Type": "application/json"
data = {
    "destination": {
        "path": new_parent_path

response =, headers=headers, json=data)

if response.status_code == 200:
    print("Folder moved successfully.")
    print(f"Error moving folder: {response.json()}")

Unfortunately, this code is not correct! In fact, the official Microsoft Graph API Documentation for the Move method says

Request body

In the request body, supply the new value for the parentReference property. Existing properties that are not included in the request body will maintain their previous values or be recalculated based on changes to other property values. For best performance you shouldn’t include existing values that haven’t changed.

Note: When moving items to the root of a drive, your app cannot use the "id:" "root" syntax. Your app needs to provide the actual ID of the root folder for the parent reference.

Truthfully I’m gutted the code didn’t work because it would have made my life simpler, but it highlights a key consideration when using Generative AI.

You always need to check the output, whether it’s facts, grammar or code. These systems are not full-proof, and it’s incumbent on you to use your critical thinking skills to validate the information you’ve been given. No matter how reasonable it sounds.

68: Retail App


  • Good news! After months of toil, we received a security sign-off last week, which means that the Retail App is going live in the next couple of days!
  • There is still one feature we’re finishing up, but that doesn’t impact the release.
  • The Retail App has, baring a few curve balls, been a good project, and I’m proud of my team for what they’ve built.
  • I’m looking to see how Three evolve its use over the coming months, but I’m confident it’s an excellent start for their plans to consolidate the retail employee digital experience.
  • Thank you! Callum Ilett, Adam Auckland, Josh Browning, Dave Inoc, Dan Maxwell, Nat Patemkina, Eleanor Morgan and Marcus Woodbridge

67: JavaScript templates strings in Python


When I first started learning Python, the one thing I wanted more than anything was to be able to use template strings, like had gotten used to in JavaScript.

language = "JavaScript"
string = `this is a string literal in ${language}`

For some reason, I couldn’t find a decent article telling me how to do them.

Then on Sunday, when I asked ChatGTP to generate a piece of code for me, I was struck by something in the output.

url = f"{TENANT_ID}/oauth2/v2.0/token"

So I asked ChatGTP what the ‘f’ meant, and it told me about ‘f-strings’ for ‘formatted string literals` in python, which is exactly what I was I was looking for.

So here you are. If you want to use template strings in python, then prefix the string with an f, and you’re done!