-
Notifications
You must be signed in to change notification settings - Fork 0
Antares Background
TL;DR Antares is a system architecture that embodies Clean Architecture principles and can help project teams tolerate greater change in requirements both business and technical.
The art of giving business requirements is one of saying what should happen, when.
The craft of development is that too, but also of creating code that satisfies those requirements.
Walking on water, and developing software from a specification are equally easy - when both are frozen. ˗ Deepak, a coworker
We have to keep up with business requirements. The problem, as we know, is that code tends to become unchangable the more we add to it.
The business goal of Software Architecture is to make the kinds of changes that are frequently requested possible, with higher speed and less risk than without.
Imagine a house. Imagine you've got to change the gutters on the house, or the siding, and find you can't without endangering the structural integrity of the basement. WHA?
The desired qualities of a Software Architecture, for our purposes here:
- Keep independence of business requirements from each other.
- Allow new requirements to be implemented independent of others.
- Keep independence of business requirements from specific technologies used.
This is what a program, or even a full distributed system (in a later Post), gets when it is built following the Antares architecture, facilitated by the open-source library Antares Protocol.
Let's explore how decoupling will help a scenario with typical requirement changes:
You are an independent software developer, who sometimes works with an agency, and picks up new clients periodically. And as you do awesome development work, you log your time. Then, on some schedule, you send invoices. Good so far? The catch is that each time you add a client, you have to change your process for submitting hours. At my last engagement I had to copy my hours into 4 different places: Into my accounting software, into the invoice email I'm sending, into my Agencies' logs, and into the client's own SharePoint time-tracking system. This causes friction at critical times - like the end of the week, when you're trying to leave at 4, and get stuck doing invoicing well into happy hour!!
Clearly, as a technologist (who likes to get paid!) I'd never argue that we don't need to send to multiple destinations, or do whatever the client requires! However I would point out that a good architecture would allow me to continue to submit time as I've always done, independent of the the idiosynchracies of the current client's exact submission process (or processes).
So we've defined the challenge the rest of this article will explore.
As a logger of hours,
in order to focus all my time on doing billable work,
the time-logging service should be an Agent that works on my behalf, to submit invoices to clients per their requirements, without complicating my process of submitting hours
The way we will do this is to set up a billing pipeline that is reactive to the hours sent, and which has pluggable renderers that we can use to fulfill a particular client's requirements. We'll also add a particular requirement, which any good architecture ought to provide, which is that the operational characteristics ought to be changeable. In other words, it should be easy to:
- prioritize or resequence renderers
- add features like rate-limiting, burst detection, retry with exponential backoff, etc...
With minimal changes to the code or its architecture. Tall enough order? Let's get started!
Or skip if you're like me. But first: Reactive, and Renderer; lets define these terms.
Reactive - This is a principle behind React, and a general principle of business and life. If I give you a task to do for me, I can either hover over you until it's completed, or I can give you the task, and give you a way to notify me, and I'll react to that notification.
Let's understand Reactive by refactoring some code that is Imperative to a Reactive style.
This is imperative code. Each step 'hovers' waiting for completion. The validation line does stuff in memory only, takes almost no time. The next two occupy a relatively long time 'hovering' for the database to save, and a queue to be written to.
# This is imperative code - each step 'hovers' waiting for completion of all previous
def saveTime timeLogEntry
validate(timeLogEntry) # 1.5 ms
timeLogEntry.save! # 125.0 ms
queue.write(timeLogEntry) # 30.0 ms
end
Though simple to write and read, code like this operates very naively. Picture the following scenario:
You're at a restaurant, and your waitstaff brings your order to the kitchen counter. The waitstaff proceeds to stay at the counter, not taking other food or drink orders - until your food is ready!
Imperative systems necessarily hit problems of scale. It is precisely by moving to being Reactive that entire classes of resource problems go away.
Promise based code is Reactive, however. The Promise
is an object that will notify us (by calling a function handed into then
) when the process is done.
// This is reactive code - each step 'reacts' to the previous one
const saveTime = (timeLogEntry) => {
validate(timeLogEntry)
.then(() => save(timeLogEntry))
.then(() => enqueue(timeLogEntry))
}
There is also the Observable form of Reactivity, which may look different superficially, but is just as Reactive as with Promises.
// Uses RxJS operators to create a combined process which happens sequentially
const saveTime = (timeLogEntry) => {
concat(
save(timeLogEntry),
enqueue(timeLogEntry)
)
}
The general idea of the Observable style is that each rendering process is: represented by a variable, can be canceled, and can react to multiple notifications over time. It's also easy to mix and match them, or adjust their operational (error-handling or timing) characteristics.
Antares will use the Observable style for reasons explained more later, but you don't need to fully understand it now, just be able to recognize it when you see it again.
Crystal clear? One last definition to go, and it's a good one. Nah, skip it
What does it mean to render something? There's the rendering of fat into soap. You may have also heard of an artists' rendering of a subject - a painting, or a statue in their likeness.
What it means to render is: to make a change to the world which you can not take back. There's no undo button on renderings. You can only create a new rendering. Think ledger entries in accounting.
Accountants don't use erasers.
Technically speaking, we know that in React you do a render
to update the UI. But are there more things that fit the concept of rendering? Writing to a database, sending an email, putting something on a queue these are all changes which we can't take back, and are often async or take a while to come back. In fact, when we consider these other ways to render (dictionary definition), we may ask if mutating the DOM is even in the same category as writing to a database?
It is. To verify that, let's conduct this thought experiment: Your site is a clothing site. In the description of a shirt, an employee inserted some language that is offensive to overweight people. At what moment do you have a Publicity problem? When the data goes into the client-side Redux store inside the browser's memory, invisible? Or when it gets painted to the DOM??
Q.E.D.
I think you'll agree that writing to the DOM is a rendering. Writing to a queue or database or email is also a rendering, and calling a webhook or other API is a rendering. You can't take back the fact that you did these things.
Getting back to our Reactive code, what's still sub-optimal about it, per our Architectural standards?
const saveTime = (timeLogEntry) => {
validate(timeLogEntry)
.then(() => saveToDB(timeLogEntry))
.then(() => addToMessageQueue(timeLogEntry))
}
First: We are not meeting our requirements of decoupling renderers from each other; failures in one can affect the other. What happens if the db save fails? Those failures will prevent the queue from being written - is this desirable? Isn't it up to the business which is more important?
Secondly: We are not free to change our codes operational behavior- we have hard-coded a sequentiality between our renderings. The save method will be changing all the time if the business changes its mind on what is to happen, and in what order, or if we add new renderings. We need this change to be located far away from our saveTime
endpoint.
Third: The submitter of hours will always wait for the sum of the run-times of all the code before they get a response from calling this function. Is this always necessary? If we've never once lost a write to the database- could we tell them "You're all set" sooner? Could we add batching, rate-limiting, or retry logic easy-peasy?
OK, we've ripped this simple code to shreds. Now it's time to envision a solution. One in which your helpful Agent can take care of all processing tasks for you, and you can react to your agent when they notify you that the work has been completed. Something that looks like this:
// Assume we had a helpful agent to abstract away all these
// back-office processes of rendering for us. If only!
const saveTime = timeLogEntry => {
let result = agent.process(timeLogEntry)
result.completed.then(() => {
alert("You're all set!")
})
}
If only we had such an agent!