10.10.0 is an approach to building enterprise apps based on the GoTTH stack ( Golang, Tailwind, Templ and HTMX). This enables the rapid development of evergreen and zero ops apps, with the intention of reducing the cost of building and running good software, and focussing spend on new features over maintenance.
Golang is used for frontend and backend development because of its backward compatibility guarantee, meaning code that is written today will be guaranteed to compile in the future with no changes (as has been the case for that last 13 years).
10.10.0 apps have almost no direct dependencies which means these apps have minimal supply chain issues such as obsolescence, patching, or supply chain vulnerabilities. The few dependences that are used have been stable for many years and have no transitive dependencies.
10.10.0 apps use technologies pioneered in digital natives, but are tailored to meet the requirements and complexities found in enterprise. Specifically:
- Favour architectural simplicity for long term maintainability, over complex high throughput requirements of digital natives
- Include enterprise required operational and security controls
10 weeks to develop
Existing approaches
Enterprises tend to have longer development cycles for features than necessary mainly due to two factors.
Complex decision-making processes
Large enterprises often have complex requirements which involve multiple departments, each with its own set of processes and priorities. Coordinating efforts across these diverse units can drive complexity in the software development workflow which has the effect of adding layers of project management, business analysis, etc and driving developers further and further apart from users. This creates large amounts of Work In Progress (WIP), long feedback cycles, and increases development time, increasing costs and the risk that the project doesn't deliver what users need.
Older, heterogenous tech stacks
Many enterprises rely on legacy systems that are deeply ingrained in their operations, which may lack the flexibility and efficiency of newer tools, receive limited support, and have accumulated technical debt over time. To compound the problem, large enterprises have typically acquired many smaller organisations over their lifetimes, leaving them with a heterogeneous ecosystem comprised of numerous different technologies. The use of such older and disparate technologies can hinder compatibility with modern tooling and reduce flexibility, requiring custom solutions and increasing time to deployment.
The 10.10.0 approach
10.10.0 adopts a simplified development workflow and a standardised, opinionated modern tech stack.
Simplified development workflow
Use the most lean development system appropriate to your circumstance, preferably something light-weight like Kanban. Developers work directly with users in actively guiding the trajectory of the product, identifying features, breaking down work into small chunks (no more than a week) and prioritising the development effort. This leads to smaller WIP, much shorter feedback cycles, and the product tracking much closer to user needs, reducing the risk of cost overruns and missing business goals.
Standardised, opinionated tech stack
Use the 10.10.0 tech stack based on a Golang and HTMX. This tech stack avoids the constant upgrade cycles of frameworks such as dotnet or spring, and sprawling dependencies with their complex supply chains.
Use the opinionated 10.10.0 application template that comes with a standardised design patterns, CI/CD pipeline, linters, and mature libraries to reduce the time and effort spent on getting started and enforcing good practices.
Optionally the tool vision can be used to bootstrap a project with this template.
10+ years evergreen
Key issues
Two key issues reduce the lifespan of enterprise software.
Volatile technologies
Many applications are built on technologies that change every few years and then trigger an upgrade cycle in their dependent apps. Common examples are dotnet, java, python and node ecosystem. Teams must continuously learn and adapt to new standards and syntax, apps sometimes require full rewrites when vendors change approaches, e.g. .NET Framework to .NET core, python 2 to 3, etc. This forces enterprises to dedicate significant resources to keep up with the pace of upgrades or risk having out of date software, exposing them to security vulnerabilities and creating a fragmented IT system with components running on different versions.
Reliance on direct and indirect dependencies
Many applications these days are built on tens, if not hundreds of dependencies, whether direct or indirect. The average nodejs dependency, for instance, has 79 other transitive dependencies, all of which have their own release cycle and compatibility considerations. This can force teams into “dependency hell”, where developers spend more time managing and resolving dependency-related issues like conflicting versions and incompatible libraries than working on new features or improvements.
All these dependencies are also potential vectors of attack – Sonatype’s 2023 report on the software supply chain industry noted that it had detected 245,032 malicious packages detected across the various open-source ecosystems in 2023, triple the number in 2022.
Our approach
We advocate building software using a small, core set of current technologies which prioritise stability and backwards-compatibility. The primary backend language that we use is Go, which has been backwards-compatible and syntactically consistent since its public release in 2009. In all that time, Go has remained on version 1, and the co-founder of Go recently reaffirmed the language’s commitment to compatibility, emphasising that “Go 2, in the sense of breaking with the past and no longer compiling old programs, is never going to happen”. With Go, entire apps can be automatically migrated to the latest version and benefit from the latest security patches and QoL upgrades, with no need for rewrites or upgrades.
We also support avoiding reliance on bloated frameworks or tools which introduce a large number of third-party dependencies. Any dependencies which we do use, such as htmx for frontend interactivity, are deliberately selected because they are lightweight, have a stable track record, and have no transitive dependencies.
Using this approach, we envision that software will remain “evergreen”, which means enterprise software development teams can write an app and "fire and forget" without having to worry about constant upgrades or supply chain issues. Updates are so stable CI/CD can take care of them automatically, and the app will continue to run for years to come.
Zero operations
Maintenance burden
Many enterprise apps come with a significant maintenance burden, requiring teams to devote time to updating, upgrading, and patching applications as well as their third party dependencies. This takes up valuable developer time and reduces capacity for new feature development. This is driven by two main factors.
Tightly-coupled infrastructure
Applications running on manually built infrastructure such as physical servers, VMs in the cloud, etc tends to create a complex system of components that rely on each other or on specific host configurations to work, making for a very inflexible and very fragile system. Time and resources must therefore be spent to maintain and troubleshoot the system just to keep it functioning, which adds little enduring value to the business. This traditional approach to IT infrastructure often results in high maintenance costs, extended downtimes, and increased risk of errors during updates or modifications. Additionally, the maintenance of such complex systems requires highly specialised knowledge usually vested in only a few individuals, creating the risk of a knowledge vacuum when these key persons leave the organisation.
Lack of automation
A large amount of a developer’s valuable time is spent on what Google calls “toil” – work that is manual, repetitive, automatable, and adds no enduring value. Toils can pervade all aspects of operations management, including deployment and release management, monitoring and logging, patching and upgrading, licence and certificate renewal, scaling, and disaster recovery. For instance, manual deployment processes require significant time investments from development and operations teams, increasing the likelihood of deployment errors leading to extended application downtime. Scaling infrastructure to accommodate varying workloads also becomes a manual headache, with developers dedicating significant time to provisioning and configuring resources.The cumulative effect is a strained development pipeline, hindered innovation, and increased operational costs as valuable human resources are diverted toward tasks that could be efficiently handled through automation.
Our approach
Managed services
10.10.0 infrastructure build is fully automated and all configuration items are tracked in source code. The application itself is containerised and deployed on fully managed (serverless) cloud infrastructure which offers automated deployment, scaling, and management of applications, removing the need for developers to perform mundane operational tasks. Additionally, isolating apps and their dependencies, and clearly defining their API boundaries, allows for them to be easily iterated on without causing breaking changes to the other apps that run alongside them.
Culture shift
Utilising these technologies is not sufficient, a culture of zero-ops
must be embedded into the development process itself, with an emphasis on creating automated systems, checks, and CI/CD pipelines to reduce the need for a dedicated ops team and minimise the risk of human error. Tasks such as the renewal of SSL certificates are not difficult to automate, yet are often left as manual chores for the operations team and forgotten about until they cause an issue. Developing apps with an emphasis on minimising manual maintenance and operations frees op teams up to focus on more valuable tasks like building new platforms, deploying new apps, and tuning performance and security, instead of mundane chores like rotating logs, adding storage, saving backups, planning maintenance windows, and so on.
By taking advantage of managed cloud services and adopting a zero-ops
culture, we believe organisations can eliminate almost all of the maintenance tasks associated with applications and infrastructure.
Quick start
This quick start will help you install the components of 10.10.0 and bootstrap a basic hello world application.
Installation
10.10.0 apps only need go to run. The rest of tools in this installation guide are to make an awesome developer experience.
Install Golang
Download go for your OS and install it.
The official instructions are here.
You should aim to have the go
command working in your terminal.
If you are using Windows, the installer should setup the path for you.
Ensure that go
is on your path, you should be able to type go in terminal
or cmd/powershell
and see the help output.
Once go is installed, you can add the GOPATH to your system path. GOPATH is where go will install any tools installed using go install
, This is typically your $HOME/go/bin
, this should be added to your system PATH to allow you to run installed tools.
Node
10.10.0 apps do not run in node but we use node to generate css with tailwind.
Download and install node
Tailwind and Prettier
We use Tailwind for styling, and Prettier for formatting.
npm install -g prettier tailwindcss
Air
For building and auto rebuild we user air
go install github.com/cosmtrek/air@latest
Templ
We use templ to generate the HTML pages from Go.
go install github.com/a-h/templ/cmd/templ@latest
Make
You must be on MacOS/Linux in order to use Make. Windows users can install WSL to use Make.
To simply run the app, run the following command from the root of the project:
make
(Optional) Install the vision cli for bootstrapping projects
Install the vision-cli tool which is used create 10.10.0 project scaffolding.
go install github.com/vision-cli/vision@latest
Install the vision 10100 plugin
vision install 10100
You should see the plugin listed when you run the command
vision plugins list
Bootstrap
The Vision tool can be used to scaffold 10.10.0 application code. It works by creating projects with a predefined template. Vision is not a dependency, but rather used as an accelerator.
For example:
Create a project with:
vision init myproject
Vision will create a myproject folder with a vision.json configuration file. Change to the myproject folder and initialise the 10100 plugin with the module name of your project in the form github.com/org/domain/subdomain...
cd myproject
vision 10100 init github.com/myorg/myapp
Generate the scaffolding code
vision 10100 generate
Vision will create a folder called myapp with the base 10.10.0 app. You can run the app with
cd myapp
make
You will be able to access the app at http://localhost:8080
Live Demo
The 10.10.0 Tech Stack
This chapter describes the technology stack used in 10.10.0 applications and explains the reasons for their selection.
The stack's application is based on Golang. This provides a number of advantages:
-
Backward compatibility. This is described in Go's website here "It is intended that programs written to the Go 1 specification will continue to compile and run correctly, unchanged, over the lifetime of that specification".
-
No external linked libraries. Go code compiles into a self contained binary with no external dependencies such as .so or .dll libraries on the operating system. This has many benefits such as a reduced need for operations personnel and platforms teams. It does however require occasional recompile and deploy to ensure security vulnerabilites are fixed, but thanks to backward compatibility, this can be automated.
-
Cloud native. Go was designed with fast compilation, startup and execution in mind. This makes Go apllications ideal for containerization and running in serverless cloud platforms. For example they can remain dormant (zero cost) and wake up within milliseconds to service a request when it comes in. Go applications are also multiplatform and can be compiled for any underlying infrastructure. All the cloud providers provide tooling and documention for automating go application containerization and deployment into their environments. Go provides a cloud efficient concurrency model, security model, and so on.
The stack's infra code is based on Terraform. This provides a number of advantages:
-
The code runs on all the cloud providers.
-
Access to a large number of Terraform modules.
Frontend
Frontends are written in golang and served by the golang http server. You will also need node as a dev depedency for tailwind. HTMX is used to make the app interactive. Apps are server side rendered, Go will serve all the HTML and CSS required to render the page. The HTML is generated from Go using templ. The CSS is generated from Tailwind.
- templ for rendering the html fragments
- htmx for AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML
- air for building on code change
- Tailwind for styling, and Prettier for formatting.
- Make for making our lives simpler
Why not ReactJS or Angular or something similar?
Thanks to Angelo Fallaria for this wonderful image
Backend
Backends are written in golang and served by the golang http server. APIs are HTTP based and business logic is dependent on standard lib only. Other dependencies may be added for example database or queue connectivity, and so on.
- Golang standard lib
- Make for simplicity
Cloud
- terraform
- make
Cloud sdks are used to interact with infrastructure such as databases, pub/sub, etc.
- (optional) Docker for building of the app container image and local running of components such as databases
- (optional) Relevant cloud sdk from AWS, Azure or GCP
Azure
Microsoft Azure have some very useful features that can be used to create a microservices platform with zero ops. Dapr architecture is a good example of this. We can simplify this further by using only ACI and Dapr where needed.
C4Context
title Simple microservices platform
System_Ext(Users, "Internet", "Users")
Boundary(az, "Azure", "Cloud") {
System(gateway, "Load balancer", "Azure Load Balancer")
Boundary(srvvpc, "Services", "VPC") {
System(microserviceA, "DAPR Microservice", "10100 App")
System(microserviceB, "DAPR Microservice", "10100 App")
System(microserviceC, "DAPR Microservice", "10100 App")
System(microserviceD, "DAPR Microservice", "10100 App")
Boundary(dapr, "Events", "PubSub") {
SystemQueue(pubsub, "PubSub", "DAPR managed")
}
}
Boundary(db, "Data", "VPC") {
SystemDb(SystemE, "Database", "App data")
}
}
CICD
- github actions
Resources
Here are some online resources you may find useful:
- Thanks to TomDoesTech[https://www.youtube.com/@TomDoesTech] for this wonderful video about the GoTTH stack
Trade-offs
Trade-offs represent how we compromise between competing factors. In software development we have no shortage of these factors and compromises, and in 10.10.0 applications we want to call them out explicitly and explain the decision-making behind the choices made.
Stability and simplicity vs. features and complexity
"Everything should be made as simple as possible, but not simpler." Einstein
Stability and simplicity apply to the functionality of the system, the internal structure and code, as well as the externals of the system such as the execution environment and dependencies. Lets cover these in reverse order:
-
The externals - Avoid trivial externals. Before getting to the nub of this question, which is the definition of "trivial", let me explain the idea. These days we have components and libraries for just about everything. It may be tempting to use one of these for a UI component you don't want to write, to handle a file manipulation that would be irritating to code, but what happens when the authors of that component decide to stop supporting it, or even worse they don't manage their vulnerabilities as well as you and then introduce a supply chain attack into your software? Using externals leaves you with a codebase that needs constant upgrading. However, it also doesn't make sense to write everything yourself. You don't write your own operating system or database software. But where do you draw the line? We consider this to be a question of judgement but as a rule of thumb, if you can write the features you need from a dependency in 1-2 weeks, then you should consider it trivial and you really shouldn't be using it.
-
Internal structure and code - Write code to meet your known requirements. Attempting to cater for future scenarios that are uncertain and subject to change anyway will lead a team to over-engineer software, making it more complex, slower to extend and usually more brittle. This problem can be exacerbated by senior engineers who are trying do the right thing to avoid problems they may have experienced on another codebase, but instead end up introducing unnecessary complexity. We love Eric Lau's hello world that uses a complex class hierarchy and the factory design pattern to decouple everything from everything else and bring it back together to print "hello world".
-
Functionality of the system - Famously 80% of features in software systems are rarely or never used, or even worse they can have a negative effect. For example at Google and Bing 80%-90% of features have a negative impact on metrics. Business users and even some product owners will ask for features, describing the final solution, instead of helping developers understand the actual problem and work together to find a simple solution. Developers should always make an effort to understand and challenge the business problem before accepting the feature request.
Community and open source vs. suppliers and support contracts
The Open Source Security and Risk Analysis Report found that 96% of codebases contain open source. It is a misconception that if you sign a support contract with a big IT supplier, that you have your "bases covered". Too often these suppliers will restrict your options to a limited set of expensive solutions, that are often the root cause of enterprises' problems. For example a support contract might cover .Net and C#, or Java and JDK, forcing you into these technology stacks, but such contracts are unlikely to cover modern languages like Golang or Rust or even JavaScript. Use stable open source software and join their communities to support and leverage support, instead of relying on a big IT supplier contracts that will cost you an arm and leg but are wholly incapable of supporting all the modern components in your software stack.
High quality developers vs. cost-optimised commodity developers
We are unashamedly developer centered. We love working in well crafted codebases written by skillful developers who take pride in their craft and their code - these codebases are easy to extend and debug. Conversely we have experienced the drag of working in poorly written codebases created by commodity developers. Commodity developers may be cheaper per day, but they take many times longer to write anything so their overall cost is higher, but also the code they write is a drag on all future developers who need to work on that codebase. Cheap commodity developers are a false economy.
Libraries vs Frameworks
For skilled developers, the choice between libraries and frameworks hinges on the desire for creative freedom and expressiveness in code. Software libraries empower experienced developers to pick and choose components that align with their vision, enabling them to craft solutions tailored to their unique requirements. This flexibility allows for more creativity which leads to better code and happier developers.
In contrast, frameworks, while streamlining development and providing conventions, restricts a developers ability to create and be expressive. The guardrails built into frameworks can prevent developers from writing code they way they want. Trying to do something in a framework that it doesn't cater for, can lead a developer to spend days searching the corners of the Internet for people who have tried to it and failed, and then spending days decompiling framework code to figure out how things are wired in order to get a simple deliverable done the way they want.
It is not uncommon to struggle to find talented young software engineers who are prepared to work in these frameworks for example .Net and Java Spring. Some organisations are trapped in these ecosystems and will keep going with an ever ageing talent pool, while others switch away from these frameworks and benefit from highly skilled developers that prefer to work with libraries.
10.10.0 is built with highly skilled engineers at its core. Frameworks are unnecessary.
Opinionated
In the previous section on Libraries vs. Frameworks we discussed how libraries offer highly skilled developers the freedom to express their ideas in code, while frameworks programatically constrain developers to prevent them from making mistakes (which ends up restricting their freedom). Having said that, highly skilled developers are famously opinionated! They want things done their way and will argue till the ends of the earth for their opinion (emacs is better than vi!). The difference with frameworks is that opinions can always be abandoned if there is a good reason, good luck doing that in a framework.
Sometimes opinions have an impact on code quality, and these must be adhered to unless there is a really really good reason to abandon them. We use
- Coding standards called The Power of 10100
- Standardised application scaffolding. We use a tool called Vision to reuse our best practice
- Standardised infrastructure. Again we use Vision to generate the infrastructure as code in Terraform or Bicep, creating a standard microservice platform that meets the needs of enterprise. Again no need to debate how to achieve scaling (Serverless or Kubernetes), no need to debate how to do databases (Managed Postgres), go with the best practice captured in Vision.
But then sometimes opinions are just a choice and the point is to select one way do it consistently (should the open curly appear at the end of the function definition or on the next line?!)
The Power of 10.10.0
This is a nod to Nasa's Power of 10 coding rules for writing safety critical systems. We like the spirit of these rules but they are not appropriate for writing enterprise applications. So here are our 10.10.0 rules for writing robust and maintainable enterprise code.
Follow the standard go practices
They can be found here
Use stdlib for business logic
Business logic must be separated from state and mutable logic. Golang's stdlib should be the only library you use for writing business logic. The go standard library is well tested and future-proof.
Avoid complex functions
Limit cyclomatic complexity to 10 (warning), and 15 (critical defect). Setup a linter to show this early on.
Keep code files small
Ideally two to three pg-dn should reach the end of the file. Where necessary this can be extended, but files with thousands of lines are an anti-pattern. Consider breaking up the files or even creating a new package to structure the code into smaller pieces. This is driven by keeping complexity low so the next developer working on the codebase can become productive quickly.
Business logic should be in deterministic functions
Lets unpack this... the idea of deterministic functions is borrowed from pure functional programming, so business logic must be separated from state and mutable logic such as retrieving data. This will make business logic easier to read and test.
Consider this function
func CustomerDiscount(id string) float64 {
c := LoadCustomerFromId(id)
// vip discount
if c.name == "Kavi" {
return 0.1
}
// pensioner discount
if c.age > 60 {
return 0.2
}
return 0.0
}
This is not deterministic and testable as LoadCustomerFromId makes CustomerDiscount stateful and unpredictable.
This is better:
// load the customer in the main control loop which calls the
// business logic function with the customer as a parameter
func CustomerDiscount(c *Customer) float64 {
// vip discount
if c.name == "Kavi" {
return 0.1
}
// pensioner discount
if c.age > 60 {
return 0.2
}
return 0.0
}
so you can deterministically test this business logic with tests like this:
func TestVipDiscount(t *testing.T) {
c := Customer{
id: "1",
name: "Kavi",
age: 30,
product: "Laptop",
}
d := CustomerDiscount(&c)
if !almostEqual(d, 0.1) {
t.Errorf("Kavi's VIP discount is not 10%%, got %.2f", d*100)
}
}
Reusable functions
This idea enhances code versatility and scalability. By encapsulating specific features at the lowest level of the software model, you can create modular components that can be easily integrated across various contexts. Consider this code
type Product struct {
name String
}
type Customer struct {
product *Product
}
func (c Customer) Discount() float64 {
if c.product.name == "Laptop" {
return 0.05
}
}
type Another struct {
product *Product
}
func (a Another) Discount() float64 {
if a.product.name == "Laptop" {
return 0.07 // !!! Oops the Customer object gets 5% but AnotherObject get 7% on laptops
}
}
It would be better for the Product object to declare its discount. This is better
type Product struct {
name String
}
func (p Product) Discount() {
if p.name == "Laptop" {
return 0.5
}
return 0.0
}
type Customer struct {
product *Product
}
type Another struct {
product *Product
}
func (c Customer) Discount() float64 {
return c.product.Discount()
}
func (a Another) Discount() float64 {
return a.product.Discount()
}
Use a style guide
We recommend Uber's style guide, but any one that is suitable will do as long as you have one.
Methodology
We have built a number of 10.10.0 applications for enterprise and share our methodology here. This is our method and serves only as a suggestion.
In 10.10.0 applications, a squad of engineers engage with a client product owner to understand a business problem. There are no layers of consultants, analysts, etc between the engineers and product owner. Engineers must understand the business problem and challenge any proposed solutions.
Once the problem and solution approach are defined, the engineering team will use Vision to spin up a project and test environment within a day. The engineers will deliver a small set of features by the end of the week and start a process of weekly feature demos with the client product owner, iterating the solution weekly and getting the application into production and in business use as soon as possible.
Engineers are responsible for writing unit tests and modifying the end-to-end tests as part of their development workflow. By thinking about testing at the start, engineers can write more testable code which is generally higher quality and provides better automated test coverage and lower defects.
Once the application meets its requirements, development can stop and the engineering team can move on to another project. If the product owner needs further features built, the team and pick it up ad hoc. There will be no need for large complex and expensive upgrade projects the next time the codebase needs to be extended.
An example timeline is shown below:
-
Week 1:
- Meet the users / product owner, and understand the business problem to be solved
- Explain the journey to the users, with specific emphasis on the demands on users time
- Develop a view of the functionality and create an initial set of standalone (no integration) feature tickets
- Bootstrap the frontend and backend code using a template
- Deploy a demo environment in the cloud
- Develop the initial set of frontend and backend tickets
- At the end the week demo the application to the users. Users will be able to 'touch and feel' the system, and will start to develop a view on how the system could be used.
- Refine and prioritise tickets for week 2, including core business logic and integration tickets
-
Week 2:
- Access to integration testing environments
- Raise any requests for the production environment
- Engage any required operational teams
- Setup CD to production
- Tickets
- End of week demo
- Users get access to test environment to test
- Define UAT tests
-
Week 3-7:
- Tickets
- End of week demo
- Refine and prioritise tickets for next week
-
Week 8:
- UAT
- Defect fixing
-
Week 9:
- Final user testing and defect fixes
- Complete any acceptance into service steps
-
Week 10:
- System is live
- live support
Authors
Steven Lee - Head of Software Development | Anastasia Starostina - Software Engineer |
![]() | ![]() |
Vivian Leong - Software Engineer | Kavi Pelpola - Senior Software Engineering |
![]() | ![]() |