Friday, 10 July 2026

Layers are the symptom of Methods

Ask someone who does not write software to describe what happens when they edit an article and press save. You will get something like:

  1. find the article
  2. change the title
  3. tell me it worked

Steps each finishing before the next begins.

Now open the code that does it:

  1. A controller receives a request DTO, validates it, and calls down
  2. A service asks a repository for the article, mutates it, saves it, and returns it
  3. The original controller then maps the entity to a response DTO and returns that

The steps are still in there. But they happen going down and up the layers. Nothing about the file layout tells you the order things happen in. The person who described the feature would not recognise the code that implements it.

The usual explanation is that real systems are more complicated than the business description and layers are how we manage the complication. I have stopped believing that. The business steps were a good description. What the technology lost was the shape and the shape was taken from us by the method call.

Here is the same feature as a function orchestration:

load:
  class: com.example.LoadArticle
  next: apply

apply:
  class: com.example.ApplyArticle
  next: respond

respond:
  class: com.example.RespondWithArticle

Find the article. Change the title. Tell me it worked. Steps in the order they happen, each a small class you can read on its own. That is the whole of it. There is no controller resuming, no service in the middle, and nothing happening on the way back up.

The interesting part is not that it matches the business steps. It is what stops being necessary once the method round trip is gone and particularly that layers stop being necessary. The transactional proxy stops being necessary, as the transaction can span the steps rather than a method call. Furthermore, aspects stop being necessary. What replaces them can start further functions rather than "magically" injecting functionality between method calls. Unchecked exceptions stop being necessary, because nothing has to climb a call hierarchy to be handled. Each of these I see was a workaround for a constraint that the method call imposed. And with function orchestration, each of them dissolves (see Inversion of Coupling Control for a more in depth discussion of coupling by the method).

The rest of this post discusses functional orchestration in more detail.

The layered sandwich is the shape of a method return

A method call is a round trip. The caller prepares, calls the method and then does further processing with the return. Effectively a sandwich around the method call. Stack enough sandwiches and you have a layered architecture.

We were taught to call this separation of concerns (presentation, service, repository). But which way round is it? Did we choose the principle and the method call happened to serve it? Or did we work around the method call and call it a principle after the fact?

What the business describes is a pipeline

Function orchestration removes the round trip. Functions run once and hand off forward. There is no way back up. The sandwich disappears and with it the reason for the layers.

In OfficeFloor a function has one of the following roles:

  • producer obtains an entity and sets it in scope
  • action changes an entity
  • responder turns an entity into a response
Every endpoint is a chain of producers then actions that ends in a responder.

Find the article (producer). Change the title (action). Tell me it worked (responder).

The non-technical description was already a pipeline. It always was. We just did not have the first class architecture capabilities to represent it.  Well, until function orchestration.

The content I showed at the top was simplified. Here is the real update endpoint with the validation and the transaction that I left out:

validate:
  class: com.example.ValidateArticle
  next: load

load:
  class: com.example.LoadArticle
  govern: [ transaction ]
  next: apply

apply:
  class: com.example.ApplyArticle
  govern: [ transaction ]
  next: respond

respond:
  class: com.example.RespondWithArticle

This is a sequence of steps. It reads top to bottom in the order it happens. It requires no layers.

Note what happened to the transaction. It is not an aspect wrapping a method call. It is govern, declared on each function spanning the transaction. This deserves more than a note and I will come back to it.

No longer is the response handled as part of the method sandwich. RespondWithArticle is a function like any other. It is not a controller resuming after its service bean returns. That distinction sounds pedantic until you notice it frees the pipeline from the synchronous nature of the method call.

Variables give a function explicit inputs and outputs

If functions do not call each other, and nothing returns a value up a stack, how does the entity get from LoadArticle to ApplyArticle?

Through a variable. A producer declares Out<T> and calls set. A later function declares @Val T and receives it. OfficeFloor matches them by type.

public class LoadArticle {
    public void service(@PathVariable(name = "id") Long id,
            ArticleRepository repository,
            Out<Article> loaded) throws NotFoundException {
        Article article = repository.findById(id)
                .orElseThrow(() -> new NotFoundException(id));
        loaded.set(article);
    }
}

public class ApplyArticle {
    public void service(@Val Article article, @Val ArticleRequest request) {
        article.setTitle(request.getTitle());
        article.setContent(request.getContent());
    }
}

Neither class references the other. ApplyArticle does not know that an article is loaded from a repository or that one was loaded at all. It just requires that an Article and an ArticleRequest are in scope and it says so in its signature.

This is the part that matters most. Consider that a single method gives its statements a local scope. Set a value on line three of the method and every line after it can use that value. State accumulates as the method runs. It is enormously convenient and likely why we keep writing long methods even though we know better.

Now try to split that method into separate "purist" functions (multiple parameters with single return type). Each function sees only its own parameters. With a single return value, a function hands exactly one value to the next one. So to carry several accumulated values forward you bundle them into a type. Then the flow grows and you need another combination, so you write another type. These carrier types model nothing in the domain. They exist to move state between functions. Anyone who has decomposed a large service method has met them, and has quietly given up somewhere around the third one.

Variables restore the shared scope. The set of live variables is the scope and any later function reads any earlier value it needs.

But it also improves on the single method and this is the real point. Inside a method a block of code uses the surrounding variables implicitly. Nothing states what that block reads or what it leaves behind. You cannot lift the block out and use it somewhere else because the set of variables it uses is implicit. They are inferred and the block of code has no interface.

A function in function orchestration declares them:

  • ValidateArticle transforms the post body into an ArticleRequest and sets it in scope
  • LoadArticle reads a path variable and sets the loaded Article entity in scope 
  • ApplyArticle reads an Article and an ArticleRequest from scope to take action of applying changes of the request onto the Article
  • RespondWithArticle reads an Article and transforms it into a DTO for sending in the HTTP Response

Those signatures are the contract. So the block becomes reusable wherever those variables are in scope. Furthermore, reuse follows the producer/action/responder pattern almost mechanically. LoadArticle serves the GET, the PUT and the DELETE. ApplyArticle applies the request body for the POST and the PUT. RespondWithArticle is shared by every endpoint that returns the Article. Each is written once and the YAML says where it appears.

That is the trade being made. A method gives you implicit state and no reuse. Variables give you the same shared scope, except the inputs and outputs are made explicit, with reuse as the consequence.

Governance is not an aspect by another name

Now back to the transaction because if the function orchestration is going to replace the layers then something has to replace what the layers were quietly doing for us.

In a layered application the transaction is a proxy. You annotate a service method with @Transactional. Spring wraps the bean and the proxy begins a transaction before the method and commits after it. The demarcation is the method boundary for no reason other than that the method boundary is the only thing a proxy can see.

Everyone who has used this has been bitten by what follows from it. The annotation only works if the call arrives through the proxy. Calling the method from another method of the same class silently skips the transaction. Furthermore, the unit of work must be a method. So when a unit of work is genuinely multiple steps you either fuse them into one method to get one transaction or you accept multiple transactions. The architecture bends around the demarcation mechanism.

Governance in OfficeFloor is declared as a YAML file in officefloor/govern/ and applied to functions by name:

# officefloor/govern/audit.yml
governance:
  class: com.example.AuditGovernance

The following governance class states the three moments of the lifecycle:

public class AuditGovernance {

    private Auditable enrolled;

    @Govern      // before the function body, once per enrolled object
    public void govern(Auditable auditable) {
        this.enrolled = auditable;
        auditable.recordEvent("governance-begin");
    }

    @Enforce     // after the governed functions complete successfully
    public void enforce() {
        enrolled.recordEvent("governance-commit");
    }

    @Disregard   // when a governed function throws
    public void disregard() {
        enrolled.recordEvent("governance-rollback");
    }
}

Begin, commit, rollback. The same three controls of a transaction. Transactions are just governance.

The differences are worth drawing out and none of them are cosmetic.

What it governs is chosen by type, not by pointcut. The parameter of the @Govern method declares an extension interface. Any managed object in a governed function that implements Auditable is enrolled automatically. Eligibility is worked out when OfficeFloor compiles the application graph, not when a weaver matches a string at class-load time. There is no execution(* com.example.service.*.*(..)) to get subtly wrong.

What it wraps is a run of functions, not a method. Look again at the PUT pipeline: govern: [ transaction ] appears on load and apply. Those functions run inside one transaction. Nothing had to be fused into one method to make that true. Nothing self invokes because self invocation is not a concept in a pipeline. Governance spans the appropriate functions because the runtime, not a proxy, is holding the lifecycle.

It is applied where the pipeline is described. The service class carries no annotation. To find out whether a function is transactional you read the endpoint file. You do not read the class, then check whether it is a bean, then check whether the caller went through the proxy, then find the pointcut, then work out whether it matches.

Administration does not have to fit through a method signature

Governance handles lifecycles that begin and end. For work that happens simply before or after a function, OfficeFloor has administration.

This is where the comparison with aspects gets uncomfortable for aspects.

An aspect is shaped by the method it advises:

  • it receives untyped, positional arguments (Object[] args) which are meaningful only if the aspect knows the signature it is advising. Furthermore, if the advice needs the entity, the entity has to be a parameter of that method. 
  • it must call proceed() so it sits on the calling thread inside that stack frame. Anything it wants to do has to be done inline and synchronously.

An administration is not shaped by any signature. It is attached before or after a function and it declares the extension interface it works with as the type of an array parameter:

public class AuditAdministration {

    @FlowInterface
    public interface Flows {
        void writeAuditRecord(AuditEntry entry, FlowCallback callback);
    }

    public void administer(Auditable[] auditables, Flows flows) {
        for (Auditable auditable : auditables) {
            flows.writeAuditRecord(auditable.toEntry(), null);
        }
    }
}

Read what it receives. Not the function's arguments but every object in scope that implements Auditable. The administration and the function it administers share no signature and no knowledge of each other. Neither has been distorted to accommodate the other.

Furthermore, read what it can do. Flows invokes further functions. This further function orchestration can have dependency injection, run asynchronously and escalate checked exceptions.  There is no method call to return, so function orchestration can add further steps to the pipeline.

There is one more thing an administration can take as a parameter and it is the one that shows how far this is from advice, a GovernanceManager. Administration can activate, enforce and disregard governance. A post-administration can commit the transaction and start a new one mid-pipeline, as a first class configured thing. In the AOP world that is the territory where you stop writing aspects and start reading the transaction manager's source code.

Administration can also read the annotations on the function it administers. The OfficeFloor Spring Boot starter implements @PreAuthorize via administration.

So the ranking is not close. An aspect gets untyped arguments, on the calling thread, inside one method, wired by a pointcut expression that is checked when the weaver runs. An administration gets typed extensions, can start further functions, can drive governance, can read the function's annotations. The first is a clever way to smuggle behaviour into a method call. The second does not need to smuggle anything because the pipeline was never a method call to begin with.

Which is why you can reason about it

Here is the practical test I keep coming back to. Take a function and ask what happens around it.

In the function orchestration you open the endpoint YAML. The functions are there by name with necessary govern. That is the answer complete in one file.

In the layered application you open the service class. You find @Transactional, so you check the class is a Spring bean and that nobody calls this method from inside the class. You find @PreAuthorize, so you work out where it sits in the proxy chain relative to the transaction, which is determined by advisor order, which is somewhere else. You search for pointcuts that might match the method's signature and hope you get it right in what methods they apply to. Then you find the annotation is on the interface or the parent class, and start again.

Both applications have cross cutting behaviour. In one it is declared where the functions are orchestrated. In the other it is inferred from annotations, proxies and pattern matching against method signatures, all of which are invisible at the point where they take effect. We got very good at the second. We should not mistake being good at it for it being good.

Exceptions do not have to climb

Now consider exceptions that are thrown at the bottom and caught somewhere on the way up. It is the clearest case of all because the language itself was bent to accommodate it.

In a layered application an exception is handled by @ControllerAdvice, which sits above the controller. For the advice to see the exception, the exception must survive the journey. It is thrown in the repository, passes through the service, passes through the controller, and only then reaches the handler.

Now recall what a checked exception requires. Every method it passes through must declare it. So a checked exception thrown at the bottom writes itself into the signature of every method between there and the top. Change what the repository can throw and you edit the service and the controller too. The signature of a method that does not care about the failure now names the failure.

This is where the unchecked exception came from. I believe not from a considered view about which failures are recoverable. It came from the cost of transit. Unchecked exceptions travel up the call hierarchy for free, declaring nothing, and once you are handling errors above the controller you need them to travel a long way. Java's checked exceptions are hard to live with in a layered application because the layers are exactly the thing that makes them expensive. So the industry gave up on them and gave up with them the one place the compiler could tell you what a piece of code can fail with.

A function orchestration has no call hierarchy so there is nothing to climb. A function throws, the orchestration short circuits, and the runtime routes the exception to a handler registered for that type:

# officefloor/escalation/com.example.NotFoundException.yml
handle:
  class: com.example.NotFoundExceptionHandler
public class NotFoundExceptionHandler {
    public void handle(@Parameter NotFoundException ex,
            ObjectResponse<ResponseEntity<Void>> response) {
        response.send(ResponseEntity.notFound().build());
    }
}

Look at what LoadArticle declared earlier in this post: throws NotFoundException, and NotFoundException extends Exception. It is checked. It costs nothing because there is no function between the throw and the handler. The handler is not above LoadArticle in anything. It is registered against a type and reached directly.

So checked exceptions become affordable again and the thing we traded away comes back. The signature of LoadArticle states that loading an article can fail because the article is not there. The compiler knows. A reader of the one function knows without reading the handler.

The handler is itself a function, which is the part I would not want anyone to miss. It takes the exception as a parameter, it has its dependencies injected, and it can invoke further functions. So handling a failure is not a matter of translating it into a response and getting out. It short circuits the orchestration that was running and it starts another one. An orchestration that ends in a failure hands over to another orchestration that deals with failures.

Which is why OfficeFloor calls this escalation rather than propagation. Propagation is what a stack does with the exception rising through method calls. Escalation is what an organisation does. The case leaves the process it was in and goes to the person whose job is that case. No business describes a failed credit check by saying it bubbles up through the department hierarchy until someone catches it. They say the application goes to the exceptions team, who have their own process, their own systems and their own authority to act. That is a handler with dependencies that starts its own function orchestration.

Every business process I have ever seen documented has a happy path and a set of exception paths that branch off it. Not a happy path plus a mechanism for unwinding it. The exception path is another business process joined at the point where things go wrong. That is what an escalation handler is. It is another case of the code being able to take the shape the business description already had.

The layer boundary becomes a data boundary

A little of the old layered architecture shape survives and only at the edges. The front function takes the request DTO. The back function produces the response DTO. That is the presentation boundary and it is now the two ends of a pipeline rather than a layer that wraps the others. The middle functions work with the entity, which resembles the service layer. Except that it is the body of the pipeline and not something that is called and returned from.

There is no data layer at all. A repository is a dependency of a function injected like anything else.

So the boundary that used to be enforced by a call is now enforced by data. Entities live in the middle. Each function declares what it works with and that declaration is the boundary. ApplyArticle alters an entity and cannot see a status code or a JSON body. This is not because a layer forbids it but because those things are not in its signature.

What you give up

I would rather say this than have it discovered.

The wiring moves out of the code and into the YAML. You cannot find the next step by clicking through in an IDE. That is a real loss and the endpoint file is the compensation. One file, one endpoint, the whole sequence visible at once.

Variables are matched by type. So two variables of the same type in one pipeline need qualifiers to tell them apart. Plus there is no single stack trace through a request because there is no single stack.

These are the costs of not having a call stack. They are the same costs, incidentally, that everyone pays as soon as they go asynchronous, or reactive, or event-driven. Function orchestration is honest about paying them up front in exchange for the shape closer to the business process itself.

Closer to how people already think

The claim I want to make is not that function orchestration is faster or more scalable. Though the absence of a blocking round trip has something to say about both. It is that the function orchestration is what the person describing the feature said in the first place.

Find the article. Change the title. Tell me it worked. Nobody has ever described an endpoint by saying that the presentation layer will map the request and then delegate to the service layer, which will begin a transaction before consulting the data layer, and that the response will be assembled as the calls unwind. That description is not more precise than the first one. It is a description of the call stack wearing the clothes of an architecture.

We spent a long time teaching ourselves to think in layers. We taught it to everyone who came after and it worked well enough that the origin was forgotten. Layers are what a method call leaves behind. Take the round trip away and they do not need replacing because the thing they were organising is gone. What is left is a sequence of functions, with the state each function reads and writes written on its face.

The full pattern is written up in the Orchestration Patterns and Naming reference, the mechanics of passing state between functions in the Variable tutorial, and the begin/commit/rollback lifecycle in the Governance tutorial. They run on Spring Boot so the comparison can be made on the same ground rather than against a strawman.

As always I would be glad to hear from anyone who has tried to decompose a service method into reusable pieces and come against the limitations of the method.

Monday, 29 June 2026

Whoever Sets the Default Sets the Architecture

In my last post I asked whether AI would keep us stuck in 2020 architectures. I argued that the model has no opinion about which architecture is better. What it has is a confidence gradient, shaped entirely by how well-specified each option is, and so the fix is to make new patterns legible: document them thoroughly enough that an assistant can follow them even though it has never seen ten thousand examples.

I still believe that. But I've kept turning the argument over, and I've become convinced the conclusion is less comfortable than “so write better docs.” If I take my own premise seriously, it leads somewhere with real consequences for who actually gets to decide what software looks like from here. Three steps get there.

“Just write better docs” is a trap door, not an exit

The strongest objection to my last post is deflationary, and I want to meet it head on because I nearly talked myself into it. If documentation is the lever, then document your existing stack better and skip the new framework entirely. Hand the model a thorough rules file for the Spring conventions you already use, and you get AI-legible code without adopting anything new.

It's a fair point, and it's wrong in an instructive way. Notice which direction that lever points. Improving the legibility of the thing that is already winning only entrenches it further. “Write better Spring docs so the AI uses Spring well” doesn't rebut my argument. It is my argument, aimed at the status quo. The deflation collapses into the claim it was meant to puncture. Documentation is never neutral; it always advantages whatever it documents. Pointed backward, it is a force for staying exactly where we are.

The lock-in is to a vintage, not a vendor

I framed last time as new-framework-versus-Spring, with OfficeFloor as the test case. That framing was too small, and it let Spring off the hook unfairly. The same gravity drags Spring's own modern patterns.

Ask an assistant to “add a REST endpoint” with no further steering and you'll reliably get an imperative @RestController, blocking JPA, and quite possibly RestTemplate, a client Spring has effectively retired. You will not, by default, get WebFlux, the functional RouterFunction style, RestClient, or anything touching the AOT and native-image work. Not because those are wrong, and not because Spring failed to document them, but because the corpus's center of mass is older than Spring's current recommendations.

I don't say this to knock Spring. I built Spring Boot in as the base of OfficeFloor v4 precisely because the ecosystem is far too good to replace. The point is that even the incumbent can't easily pull its own users forward. What AI locks us into isn't a vendor at all. It is a vintage, roughly mainstream Java at peak Stack Overflow. Spring-introducing-WebFlux faces a gentler version of the exact headwind OfficeFloor faces. This is not a competitive disadvantage for newcomers; it is a structural tax on novelty itself.

Documentation lifts the ceiling, not the floor

This is the distinction I understated last time, and my own experiment shows it. When I worked with an assistant to convert Spring PetClinic REST over to the OfficeFloor YAML approach, documentation was the bottleneck, and once the docs were good enough the conversion went through. But there was a quieter result I didn't dwell on: left to its own judgement, with no steering, the assistant reached for @RestController every single time.

That's the whole thing in miniature. Documentation does real work, but only one kind: it makes a pattern usable when explicitly asked for. That's the ceiling. It does nothing to the pattern the model reaches for unprompted. That's the floor. The docs are what let the assistant produce something other than a controller once I told it to. They never changed what it volunteered when I didn't. So even a perfectly documented new architecture stays opt-in, competing against a default that is opt-out. Docs let an innovation be chosen. They don't make it get chosen by default, and they don't make it get discovered by anyone who didn't already know to ask. Necessary, but nowhere near sufficient.

And the floor is sinking

Now make it dynamic, which is the part that worries me most. If AI writes a growing share of new code, and that code clusters on the center of mass, then the next training corpus is more concentrated there, and the next model's default is stronger. The patterns a model reaches for unprompted don't merely persist. They compound, as models increasingly train on their own most common output.

That isn't static lock-in. It is a ratchet toward the mean: the space of architectures a model will volunteer narrows over time, generation by generation. The conservative force I identified last time is self-reinforcing. Left alone, “what's common” and “what's good” don't just differ. They actively diverge.

So who sets the default?

If documentation only lifts the ceiling, then the thing that actually decides which architectures survive is whatever sets the floor: the agent's system prompt, the project's rules file, the scaffolding and templates a tool ships with, and underneath all of it, the model's priors. A single line in a rules file can flip the default outright:

# AGENTS.md / rules file
When adding a REST endpoint, define it as a YAML endpoint file
under src/main/resources/officefloor/rest/, one function per step.
Do not generate @RestController classes.

That relocates architectural power in a way worth naming plainly. It used to sit, in theory at least, with “the best idea wins on merit.” It now sits with whoever configures the agents. My “is this idea legible to an AI” was the entry ticket; it only gets you into the room. The harder version of my own point is this: legibility gets you considered; only the default gets you chosen.

What follows, including for me

This changes what I think I owe you as the author of OfficeFloor. The job isn't finished when an assistant can use YAML endpoints because I documented them well. It's finished when an assistant reaches for them without being told, which means the starter should ship with the agent rules and scaffolding that make function injection the default, not just tutorials that make it possible. The battleground is the configuration my users actually load, not the tutorial they may never read.

For teams, the implication is broader: your architectural decisions increasingly live in your agent configuration, not only in your codebase. An unmanaged default is still a decision, a decision to ship 2020. The rules file deserves to be treated as a first-class architectural artifact and reviewed like one.

And for all of us: be a little wary of the ratchet. “The model wrote it this way” is evidence that a pattern is common, not that it is good, and those two properties are now drifting apart on purpose.

AI won't keep us in 2020 by holding opinions. It will keep us there by holding defaults, and defaults are always set by someone. The question I opened isn't really whether my idea is legible. It's who sets the default when nobody is steering, and that turns out to be less a technical question than a political one. I'd rather we decided it on purpose than inherited it by accident. As before, I'd be glad to hear from anyone seeing the same thing, especially anyone who has tried to win the default for a new pattern and found out what it costs.

Sunday, 28 June 2026

Will AI keep us stuck in 2020 architectures?

Every time I sit down with an AI coding assistant, I notice the same thing: it is very good at Spring. Annotations, profiles, @Autowired, the whole call-stack-driven dance of beans wiring into beans. AI has seen twenty years of this. It guesses well, even when it has to infer how a profile-specific bean is going to be selected at runtime. This is because it has seen ten thousand examples of exactly that pattern.

Which raises an uncomfortable question for anyone working on a new architecture: if AI is this fluent in 2020-era patterns, are we as an industry going to stay locked into those patterns simply because that's what the model knows? Is AI a conservative force that quietly drags software architecture backwards to its training data's centre of mass, no matter how good a newer idea might be?

I wanted to find out, using my own project as the test case.

The bet: an explicit index beats an implicit one

OfficeFloor version 4 added a feature I think is genuinely interesting for the AI era: REST endpoints can now be defined in YAML files, sitting alongside your existing Spring Boot code, with the directory structure following the URL structure. A file at greeting.POST.yml defines POST /greeting. A file at greeting/{name}.GET.yml defines GET /greeting/{name}. Inside that file, you compose the small functions that handle the request:

# greeting.POST.yml
validate:
  class: ValidateGreetingLogic
  outputs:
    valid: build
build:
  class: PostGreetingLogic
  next: audit
audit:
  class: AuditGreetingLogic

Each function still gets its dependencies injected by Spring exactly as it always has. OfficeFloor doesn't replace Spring's DI, persistence, security, or actuator setup. What changes is the flow. In a typical @RestController, the order in which validation, business logic, and auditing run is implicit: it lives in the call stack (in if statements and which methods call which other methods). To understand it, you read code. To change it, you read more code, because the wiring isn't written down anywhere as data; it's compiled into control flow.

In the OfficeFloor YAML version, that wiring is the file. Conditional branches, sequencing, error flows: they're declared, not buried. No function in the chain knows about the others. No annotation is describing the relationship from inside a class. The YAML is a complete, readable specification of how the endpoint behaves, sitting right next to the endpoint's own URL path in the directory tree.

This is essentially Function Injection, the same move Dependency Injection (DI) made decades ago, but one level up. DI took "what do I depend on" out of imperative constructor code and made it an explicit, configured, first-class concern. Function Injection takes "what happens next" out of the implicit call stack and makes that explicit and configured too. It's a continuation of the Inversion of Coupling Control idea I've been writing about for years: Dependency Injection only ever solved one slice of the coupling problem. Control flow coupling was always still there, just invisible.

For a human reading the code, this might feel like a wash, maybe even a step backwards.  This is exactly why the industry settled on annotations next to code in the first place; developers wanted the wiring close to the implementation, not off in some separate descriptor. That preference made sense when humans were the primary readers doing the navigating.

But an AI assistant isn't a human reading top to bottom. An AI assistant is trying to find the minimum context needed to make a correct, surgical change, and that's a search and navigation problem, not a stylistic one. A YAML file that names every function in an endpoint's execution path, in order, with explicit conditional branches, is a search index. The AI doesn't need to read the rest of the code base to be confident it has found everything relevant to that endpoint. It opens one small file and the entire behavioural contract of that URL is sitting right there.

That's the theory, anyway. I wanted to know if it would actually hold up against a model that has been trained almost exclusively on the other way of doing things.

The experiment: converting Spring PetClinic REST

To test this for real, rather than on a toy example, I took Spring PetClinic REST, the long-standing reference REST implementation of the PetClinic sample app that the Spring community has used for years, and worked with AI to convert its endpoints over to the OfficeFloor REST YAML approach.

It did not work on the first attempt. It took about five iterations to get a clean conversion, and the bottleneck wasn't OfficeFloor's runtime, and it wasn't really the AI's coding ability either. It was documentation. Each attempt surfaced a gap in the tutorials: some assumption I'd left implicit because it was obvious to me, a place where the YAML schema's possibilities weren't spelled out, an edge case in how a Spring @RestController-style behaviour should map across. I used the AI's confusion as a signal: where it guessed wrong or asked the wrong question, that was exactly where the tutorial needed another paragraph, another example, another explicit rule. Five rounds of "AI gets stuck, tutorial gets fixed, try again" later, the conversion went through cleanly.

I recorded the final working conversion. You can watch it here:


Spring PetClinic REST to OfficeFloor REST YAML

You can also see the resulting changes in the forked repository pull request.

So which is it: does AI lock in 2020 architecture, or not?

Both things turned out to be true, depending on what's actually being asked of the AI.

Where AI defaults to what it knows: left to its own judgement, an AI assistant will reach for Spring conventions, because Spring conventions are the statistically dominant pattern in its training data. If you ask it to "add a REST endpoint" with no further steering, you'll get an @RestController and an @Autowired field, every time. That's not a flaw in the model. It's just what twenty years of public code looks like, averaged.

Where AI happily adopts something new: the moment the new pattern is clearly and completely specified, the model's prior training stopped being an obstacle and became almost irrelevant. AI doesn't need to have seen ten thousand examples of a YAML-driven REST framework to use one correctly. It needs an accurate, complete description of the schema and the conventions, and then it follows that description. The five-iteration process wasn't really "teaching the AI to think differently." It was closing the gaps between what I assumed was obvious and what was actually written down anywhere the AI could read it.

That reframes the original question. The risk isn't that AI is architecturally conservative by nature. The risk is that new architectures rarely come with documentation anywhere near as exhaustive as Spring's, because Spring's documentation had two decades and a vast community writing tutorials, blog posts, Stack Overflow answers, and books about it. A new approach starts that race from zero. If its docs stay thin, AI will keep defaulting to Spring patterns, not out of preference, but because Spring is simply the only option it has enough information about to be confident in.

So the honest answer is: AI won't keep us in 2020 architectures by itself. But it will, by default, if nobody does the work of making the alternative legible to it. The model doesn't have an opinion about which architecture is better. It has a confidence gradient shaped entirely by how well-specified each option is in what it's been able to learn or been given.

The interesting part for framework and tool authors

If this holds generally, and I'd be curious whether others doing similar work see the same thing, it changes the calculus for anyone designing a new way of building software in the AI era.

It used to be that the cost of an explicit, separated configuration artifact (think XML wiring files, or graphical configuration tools) was paid almost entirely by human developers, who found it slower to read and slower to navigate than code-adjacent annotations. That cost was real, and it's a big part of why annotation-driven frameworks like Spring won the last decade.

AI changes that cost calculation. An explicit, structured index, a YAML file that names every function and every transition in an endpoint, located exactly where the URL structure says it should be, costs an AI assistant almost nothing to read and a great deal less to get wrong, because there's no implicit call-stack archaeology required. The structure that used to be a tax on humans is now a gift to the thing increasingly doing a large share of the maintenance work.

But that gift only arrives if someone pays a different tax: writing the documentation thoroughly enough, and unambiguously enough, that an AI assistant can pick up the new pattern from the docs alone, the way it picked up Spring from a decade of incidental exposure. Architecture innovation in the AI era may end up being gated less by "is this a good idea" and more by "is this idea legible to an AI that has never seen it before." That's a genuinely different bar than the one we used to optimise for, and it's one I think is worth more people paying attention to.

If you want to look at the actual schema, the tutorials, or try the conversion yourself, the starting point is the OfficeFloor REST tutorials, and the Spring PetClinic REST source is on GitHub if you want to attempt your own conversion and see where your AI assistant gets stuck. That's usually exactly where the next improvement to the docs needs to go.

OfficeFloor v4: from one graphical file to one YAML file per endpoint

OfficeFloor has always been about Inversion of Coupling Control: not just injecting objects, but injecting the functions and threading too. For a long time, the way you expressed that wiring was graphical, a single configuration file with boxes and lines showing how your functions connected together.

It looked great in a demo. It did not survive contact with a team.

The problem with one big graphical file

A single graphical configuration file is fine when one person is building the application. The moment a second or third developer starts working on the same project, that file becomes a bottleneck. Two developers add two different endpoints, both touch the same file, and now someone has to merge a diagram. Graphical formats don't merge the way text does. There's no good way for a merge tool to reconcile two sets of moved boxes and re-routed lines. You end up resolving it by hand, in the underlying XML, squinting at coordinates and generated identifiers to figure out what actually changed.

That's a hard sell to any team used to git merge just working.

One YAML file per endpoint

OfficeFloor v4 replaces the single graphical file with one YAML file per REST endpoint. The file name itself encodes the HTTP method and the URL path, so the configuration and the routing are the same thing:

src/main/resources/officefloor/rest/
  greeting.GET.yml        →  GET  /greeting
  greeting.POST.yml       →  POST /greeting
  greeting/{name}.GET.yml →  GET  /greeting/{name}

Inside the file, named steps wire functions together, each naming the Java class (and, where needed, the method) that implements it:

validate:
  class: ValidateGreetingLogic
  outputs:
    valid: build

build:
  class: PostGreetingLogic
  next: audit

audit:
  class: AuditGreetingLogic

That's the entire specification for a three step pipeline of validate, build, and audit, with conditional branching and sequential composition both declared right there in the file. None of the three classes knows about the other two. The YAML is the only place that knows the order and the wiring.

Why this works so much better for teams

Each endpoint now lives in its own small text file. Two developers adding two different endpoints touch two different files, so there's nothing to merge. Even when two people do need to touch the same endpoint, it's a small YAML file, not a generated diagram, so a normal text merge actually works. When it doesn't, the conflict is a few readable lines rather than a tangle of graphical coordinates.

It also turns out this same property of small, explicit, readable files is exactly what helps AI coding tools. An endpoint's steps, their order, and their branches are all explicit in one file rather than implied by a call stack. That gives AI tooling (and, frankly, any developer new to the codebase) a direct index into the small, focused functions behind each step, rather than something it has to reconstruct by reading framework conventions.

Built on Spring Boot, not instead of it

This change comes with v4 moving OfficeFloor onto Spring Boot as its base. Add the starter to your existing pom.xml, and you can start declaring endpoints as YAML files alongside your existing @RestController classes, with no migration required. Spring's dependency injection, security, persistence, and actuator configuration are untouched. Spring beans are injected into your step methods exactly as they would be into a controller. OfficeFloor isn't replacing Spring; it's taking over just the wiring of REST endpoints, where the graphical file used to live.

<!-- Add to existing pom.xml -->
<dependency>
  <groupId>net.officefloor.springboot</groupId>
  <artifactId>officefloor-rest-spring-boot-starter</artifactId>
  <version>4.0.2</version>
</dependency>

Where to go next

The tutorials walk through this in detail, starting with Spring REST for the basics of YAML endpoint files, and the Function Injection tutorial for multi step pipelines with conditional branching. From there the tutorials cover validation, exception handling, Spring Security, OpenAPI generation, thread injection, and more, all configured the same way, one YAML file per endpoint.

If you've used OfficeFloor's graphical configuration before, this should feel familiar in spirit and much easier in practice. The functions are still composed, not coded together. They're just composed in a format your team's tools already know how to handle.

Wednesday, 6 January 2021

Performance Improvements for TechEmpower Round 20

OfficeFloor is turning it's attention to improving performance now that it's underlying concepts have been defined.

OfficeFloor primarily is Inversion of Coupling Control to avoid the OO Matrix.  This goes beyond dependency injection to include function/continuation injection and thread injection. In our writings, we've come to understand the fundamental model.

However, while some frameworks claim functionality is more important than performance, we at OfficeFloor believe that's just incomplete solutions leaking cumbersome work arounds.  Take for example, aspect oriented programming in Spring, where the implementation is layers of reflective calls that hamper performance. In OfficeFloor, everything is executed in discrete functions, or more correctly first class procedures. This results in the application being a list of functions executed one after another.  An aspect is just a function inserted into the list.  Therefore, the cost of an aspect in OfficeFloor incurs no additional overhead costs.  But yes, we admit, there is overheads managing the execution of the list of functions.

So now that the inversion of coupling control solution is understood, the focus has been to look at optimisations.  And what better way to look at improving performance than competing against other frameworks.  Hence, we very much appreciate the work by the TechEmpower Benchmarks.

However, truth be told, OfficeFloor's focus has been both on improving optimisations and increasing third party integrations to inject into OfficeFloor applications.  So from round 19 to round 20 of the TechEmpower Benchmarks, OfficeFloor has added ability for asynchronous programming.  This has opened the door to using technologies such as Reactor and in particular R2DBC for asynchronous database interaction.

Asynchronous frameworks seem to dominate the top of the TechEmpower Benchmarks.  Now with Project Loom claiming negligible threading overheads this may change in future years.  However, we doubt a model of "negligible threading contention" will out perform the no threading contention of the single threaded asynchronous frameworks.  But this is just conjecture without real numbers to back it up.  So let's focus on the real numbers of round 20.

The most interesting inclusions by OfficeFloor for round 20 are:

  • officefloor-raw using R2DBC to provide implementations for the database tests
  • officefloor-async providing R2DBC solution
  • officefloor-raw using thread affinity to increase throughput
  • ByteBuffer pooling improvements
  • using parallel GC rather than Java's default garbage collection

OfficeFloor Raw using R2DBC

The officefloor-raw entry is different to the other OfficeFloor entries, as it's focus is purely on the HTTP server implementation.  It does not include the OfficeFloor inversion of coupling control framework.  It just provides a custom test implementation using OfficeFloor's HTTP server component.  Hence, why it is classed as a platform rather than framework in the tests.  This, however, allows seeing the performance overheads of OfficeFloor's inversion of coupling control framework.

Given we are looking to see the overheads, we provided officefloor-raw implementations using R2DBC for the database tests.  As officefloor-raw does not incur the inversion of control overheads, it is by nature going to be a lot faster than the rest of the OfficeFloor entries.  However, the closer we can get the other entries to officefloor-raw, the more we reduce OfficeFloor's inversion of coupling control overheads.

Though looking at the Java competitors above OfficeFloor, we will also consider supporting Vertx's PgClient for Round 21.

OfficeFloor Async

As mentioned, the major change from round 19 to round 20 for OfficeFloor, is the support of asynchronous programming.  The officefloor-async entry is using this functionality with R2DBC.

Now OfficeFloor is a multi-threaded framework.  The asynchronous functionality, therefore, has thread synchronising overheads that single threaded asynchronous frameworks do not suffer.  Well the developer suffers in a more complex programming model. However, the performance of single threaded asynchronous frameworks is optimal due to little contention.

Hence, the officefloor-async entry shines in the update test.  The update test is heavily database dependent and requires reduction of contention between connections. Therefore, pipelining database queries over lower number of connections will decrease the number of parallel updates.  This subsequently reduces the contention in the database and improves overall throughput.

The officefloor-async entry out performs the other OfficeFloor framework entries in the update test because the database contention is significantly more important than the threading contention for throughput.

Thread Affinity

OfficeFloor supports Thread Affinity in it's inversion of coupling control framework to pin servicing of requests to a single CPU.  This ensures optimal use of CPU caches, as moving threads between CPUs causes overheads in migrating data to the other CPU caches.  By using thread affinity, the operating system's thread scheduler is informed to only schedule the thread to run on a particular CPU.

For round 20, we have used thread affinity for the officefloor-raw entry to pin each socket listener thread to a particular CPU.  This effectively allows running as 32 separate servers in Citrine, rather than one big congested server grabbing at the work.  As contention is always overhead, using thread affinity to run isolated without contention (ok bus aside) has provided increased throughput.

ByteBuffer Pooling

The ByteBuffers to read and write from the socket have seen improvements in their pooling.  Creating and destroying direct ByteBuffers is expensive, hence it is much better to pool them for improved performance.  Furthermore, as direct ByteBuffers are separate memory to the heap, they allow OfficeFloor to effectively manage memory (rather than relying on the "dark magic" of garbage collection).

OfficeFloor has two levels of pooling the ByteBuffers:
  • single core pool
  • thread local pool

The single core pool is the shared pool of ByteBuffers all threads can draw from.  Previously, access to this pool was via synchronized methods.  This has been enhanced to use ConcurrentLinkedDeque to reduce thread synchronising overheads.  This has provided improved throughput to access the core pool.

The thread local pools are an individual pool per thread.  As the pool is specific for the thread, there is no thread contention in obtaining/returning ByteBuffers from/to the pool.  This further improves performance of threads heavily involved in reading/writing to the sockets.  However, can cause out of memory if many threads are involved in reading/writing from sockets (which can be the case particularly for writing responses by request servicing threads).

Improvements have also been made in the thread local pooling to keep memory down and allow better distribution of ByteBuffers.  This is achieved by only allowing the socket listener threads to pool ByteBuffers on their threads. As the majority of ByteBuffer interaction is undertaken by a constant set of socket listener threads, this means they can pool a significantly higher number of ByteBuffers for improved performance.  It does mean servicing threads do need to use the core pool for the response ByteBuffers.  However, as servicing threads spend time doing other work, there is less chance of contention in accessing the core pool.  Therefore, with the improved core pool access, less contention for core pool and the socket listener threads now able have higher pool sizes, it overall provides greater throughput.

Parallel GC

While Java is optimising garbage collection for large heaps, focus of OfficeFloor is on reducing garbage.  Creating lots of objects incurs more GC, which is overhead taking away from performance.  Hence, in reducing object creation there is little need for large heaps.  Therefore, while OfficeFloor has moved from Java 11 (round 19) to Java 15 (round 20), we have fallen back to Parallel GC for improved throughput due small heap size.

Note we did not notice any significant difference in Java 15 avoiding biases locking by default.  However, this was not extensively tested, as we had to do this locally (we did not have enough continuous runs before round 20 is being published to properly check this - so went with Java 15's claim indicating it should be negligible impact).

Learning from this is that for Java 15 consider using the Parallel GC if your heap sizes don't grow large.


Future Work For Round 21

For round 21 we will be looking at flattening of the call stack within OfficeFloor.  Work is already underway on this.  This will reduce the number of methods called to execute each function in the list.  Ideally, bringing OfficeFloor entries closer to officefloor-raw.


Sunday, 15 November 2020

OfficeFloor: going beyond Dependency Injection

This was a submission to DZone's Computer Science Fare:  https://dzone.com/articles/officefloor-going-beyond-dependency-injection

 
So why another Dependency Injection (DI) framework?  Because Dependency Injection is only part of the Inversion of (Coupling) Control problem.  OfficeFloor provides the complete injection solution.


Dependency Injection Problem

Dependency Injection on it's own can actually promote lower cohesion and higher coupling.  Why, because Dependency Injection provides undisciplined short cuts to get references to objects.  I need a repository to retrieve some data, I just inject it.  I need some logic to work out some result, I just inject it.  Overtime with on going improvements to systems, everything starts to reference everything else.  Changing the interface of one thing becomes difficult, as so many things just pull it in (higher coupling).  Furthermore, because it is so easy to "just inject" dependencies, convenience starts polluting single purpose logic of classes (lower cohesion).

Now, in disciplined development, we use principles such as SOLID to attempt to reduce this problem.  However, we are continually fighting Lehman's laws of two general concerns:

  1. systems need to continually add new functionality to stay relevant
  2. new functionality increases complexity reducing ability to change the system

Too often deadlines, budgets, even just bad days, we just keep enhancing classes without paying much attention to the second problem.  This all builds up and eventually we either:

  • require an expensive rewrite of the system (typically not feasible by the business)
  • live with degraded ability to add new features (potentially becoming the death of the system as it can no longer stay competitive)

Furthermore, Dependency Injection just looks at the static structural concerns of how objects reference each other.  We have not added into the mix threading or how we can compose methods together (effectively gaining benefits of functional composition).  We find this problem so systematic in DI only systems, that it has actually pulled the OO Matrix veil over our eyes.


Going beyond Dependency Injection

This is where OfficeFloor is taking things beyond Dependency Injection.

OfficeFloor injects dependencies not only into objects but also as arguments into  methods.  This means we do not have to structurally link all objects together for the method to traverse these structures.  The method just adds a new parameter for a new dependency.  The result is we avoid high coupling because objects no longer require structural references to all other objects.

However, this only addresses the low coupling, as we also want higher cohesion.  Well, OfficeFloor not only injects objects into methods, it also injects other methods into methods.  This effectively provides function composition, a concept OfficeFloor has termed Continuation Injection (or easier to remember Function Injection).  The result is higher cohesion of methods.  Methods can focus on single purposes and be composed together to achieve more complex functionality.

Note that due to OfficeFloor's containerising of the execution of methods, OfficeFloor can also containerise Monads to be composed in with methods.  This allows blending Object Orientation with Functional Programming in the same application.  We are still exploring this aspect, but the composibility provided by OfficeFloor keeps methods focused on single purposes resulting in significantly higher cohesion.

Going even further

Now we could have stopped there, as we are gaining significant benefits over Dependency Injection.  However, this only lets you build more maintainable systems.  We also want to build highly performant systems too.

Now multi-threading is hard.  Frameworks usually opt for a single model of threading (thread-per-request / asynchronous single threaded).  Attempting to blend threading models requires senior developer skill sets.  Furthermore, these blended solutions can run into tricky problems as further incremental functionality is added by other less skilled developers.

OfficeFloor, allows assigning execution of the individual methods to particular thread pools (what it terms Thread Injection).  This allows blending various threading models (even thread affinity patterns) to achieve the right mix of performance and ease of development.  OfficeFloor even allows for different execution profiles based on different hardware.  This provides highly performant applications.
 

Elephant in the room

Regardless of all these injection patterns, the real elephant in the room for applications is growing complexity.  To really address the second general Lehman issue of complexity, we need to be able to see the problem.
 
Lines of code one after another requires us to mentally visualise the complexity in the code.  As we practice small incremental changes, complexity becomes hidden in the scroll blindness of the vast lines of code.  Yes, we modularise and use principles.  However, deadlines and budgets focus us on that small new feature and not the growing complexity elephant in your system.

Furthermore, this level of complexity becomes hard to explain to non-technical individuals who generally hold the purse strings or set priorities.  As code is already too complex, I find we start drawing diagrams to communicate the problem.  We then use these diagrams to justify "refactoring" efforts to simplify the code, with promises that we will be able to add new features faster after this.  What's even more "fun" is trying to provide estimates on the effort of the refactoring.  When opening the Pandora's box of tackling the complexity, I find in DI only systems, the uncovered herd of complexity elephants just trample over those estimates.

However, by the time you find the need to draw diagrams to start to communicate the complexity problem, it is all too late. Complexity has already become a problem.  The slow creep of new functionality has added complexity to the point technical debt may become interest only payments.  Basically, complexity is showing the signs the system is creaking to it's demise.

This late discovery of complexity goes against modern software practices.   We want early feedback on this complexity risk so that it can be addressed before it becomes a problem.

OfficeFloor's configuration of wiring the methods together is graphical.  This graphical configuration visually demonstrates the complexity of the application.  The more spaghetti the diagram looks, the more need to spend effort simplifying the system.  As this is visual from the start of the project and always up to date (as is the actual configuration of the system), complexity is easily seen.  This means complexity can be addressed before it starts becoming a problem.

So our motivation behind OfficeFloor is to provide the necessary improvements over Dependency Injection to ease the building of systems, ease the maintaining of systems, and avoiding the complexity increasing so that developers keep their hair when trying to wrangle in that new bit of functionality.

Inversion of Coupling Control

So you might ask, why aren't Dependency Injection frameworks adopting Function Injection / Thread Injection for a complete Inversion of Coupling Control solution?

Because Function Injection and Thread Injection are significantly harder to implement than just injecting objects into each other.

To achieve method injection, we needed to containerise the execution of each method.  The container:
  1.  Ensures all dependencies are available for the method
  2.  Delegates the execution of the method to the appropriate thread pool
  3.  Handles all exceptions coming from the method
Now, this is only the start of the problem.  Once we containerised the method, we needed to type the method.  To compose the methods together, we needed to know what dependencies on other methods and objects the method requires.  The result is a typing system for methods that allows them to be visually represented in the graphical configuration.  This then lets the wiring of the methods be graphically undertaken.

The model driven design issues of using graphical configuration also needs to be avoided.  This is achieved by the graphical configuration being modularised similar to class source files.   They are:
  • modularised into separate files
  • each modular file is then typed, so can be included like typed methods in other modules 
  • within each module there is a flat structure (this avoids hierarchies that cause source code merge issues - merge is just like flat lines of code)
  • the flat structure is sorted on save, so editing graphical configurations results in the same stored XML (avoiding duplications due to source code merging different line orderings) 
  • contain no generated identifiers causing merge differences between developers
Finally the backing XML to the graphical configuration is easily readable for those rare occasions of having to resolve merge conflicts.

Performance requirements dictated that we could not keep swapping threads for each method executed.  Hence, if the next method was to be executed by the same thread pool, the current thread would continue on to execute that method.  This meant that thread context switches only occurred when required and not for every method execution.  This allows OfficeFloor to act as both asynchronous single threaded (no context switch) and thread-per-request (when using thread pools for method execution).  However, as threading can be tailored to specific methods, there are various custom threading models available to improve performance of your application (even including thread affinity).

Building this flexibility in threading allows the containerised methods to be executed by any thread. Hence, memory synchronisation issues became important.  To avoid significant locking and memory synchronising between threads, the internals of OfficeFloor is written mostly via immutable structures.  This allows significantly reduced locking when executing the methods within the containers.

Furthermore, as methods are not statically linked like objects, there is need for fast look up of dependencies.  OfficeFloor provides a compiler that transforms the modular configuration into a graph to represent the entire application.  This graph is checked for type correctness so that invalid application configurations is detected at compile time.  Once deemed valid, the graph is compiled down into the execution engine that uses array index lookup of all dependencies for performance (avoiding hash map look ups by strings).

Show me the working code

The following provides working code demonstrations of OfficeFloor:

 

Conclusion

So next time you look to build an application, consider OfficeFloor.  With the enhancements of Function Injection and Thread Injection over Dependency Injection, OfficeFloor encourages significantly lower coupling and higher cohesion in your applications.  This avoids complexity from creeping up and can keep you adding new features long after a DI only system become frozen from complexity. See the tutorials to get going with OfficeFloor.


Sunday, 29 March 2020

Migrating Spring Application to OfficeFloor

This series of articles looks at migrating Spring applications to OfficeFloor.

Why?

So the first question comes up.

Why migrate Spring applications to OfficeFloor?

Spring provides dependency injection to create a graph of objects. Spring, however, provides little management over threading and attempts to model behaviour through object composition. This is where OfficeFloor in it's Inversion of Coupling Control provides injection management of threading and functional behaviour.

Now this is not to say Spring is inferior.  Spring's focus has come from and stays with mainstream Object Orientation.  OfficeFloor has merely taken these Object Orientation concepts and extended them with Functional Programming composition concepts and Process/Threading concepts.

Now if you are happy with mainstream Object Orientation then by all means stick with Spring.  Spring is a great Object Oriented framework that deserves its success.

However, if you want to avoid writing your own threading or would like more seamless support for functional programming, then please read on.

Let's not replace Spring

It would be unreasonable to port a Spring application in its entirety.  Spring has been under active development since the turn of the millennium.  Ok, at the time of writing this article that is just under two decades. For some industries that could be considered a short time.  However, in the IT industry, where there are new JavaScript frameworks popping up every day, this longevity is testament to Spring's success.

For me, Spring's success has been because it got something fundamentally right. Now we could argue academically about Object Oriented composition vs Functional Programming composition.  We could argue about libraries vs frameworks.  We could even argue about Java vs new languages like Kotlin. But what I see in these arguments is focus on finding simpler and easier ways to write software.  And for me, Spring (and more specifically Spring Boot) did that by getting rid of the repetitive plumbing code.  This is so we could get on with writing the important functionality of our applications.

To enable focus on removing repetitive plumbing code there are a few things required:
  1. Extensible framework
  2. Plugins written for supporting the various infrastructure / existing solutions
  3. Open platform for community addition / management of plugins
Spring by wiring objects together enables this extensibility.  New functionality can be included by wiring in the objects to support that functionality

Plugins then extend the framework to cover including various infrastructure and existing solutions.  The plugins avoid us re-inventing the wheel so our focus can be on achieving the functionality of our application.

The final aspect is supporting the IT ecosystem. There are so many competing technologies and even platforms with the various cloud providers. A single group or company attempting to support this would be overwhelmed. Therefore, Spring has developed a strong open source community to write and maintain all the plugins. Even VMWare's acquisition of Spring mentions this open source community.

Therefore, it is unreasonable to rewrite Spring into something new.  There is too much of an ecosystem to replicate.

Integrating Spring

So rewriting everything Spring into OfficeFloor is not a feasible direction.

Rewriting is also a violation of the first point - an extensible framework.

OfficeFloor must subsequently be extensible to plugin Spring.  OfficeFloor supports extension via Object Orientation, Functional Programming and Threading.  Therefore, if OfficeFloor is not extensible via Object Oriented Spring, then OfficeFloor fails at being extensible.

OfficeFloor supports plugging in Spring via a SupplierSource.  This extension point of OfficeFloor allows for third party dependency injection frameworks to make their dependencies available for injection in OfficeFloor applications.

Now OfficeFloor's website provides detailed tutorials on various aspects.  OfficeFloor likes to adhere to the DRY (don't repeat yourself) principle, so this article series will direct you to the relevant tutorials for detailed aspects of Spring to OfficeFloor migration. Therefore, please see the Spring tutorial for how to plugin Spring dependencies into OfficeFloor.

The result of plugging Spring into OfficeFloor is that OfficeFloor gets a library of pre-written objects.  Spring focuses on wiring together a graph of objects.  OfficeFloor does not interfere with this, as Spring does this exceptionally well.  What OfficeFloor does is allow reference to the various objects in the Spring graph for injection into its own application.  This effectively allows OfficeFloor to be extended with Spring managed objects.

Going beyond Objects

The rest of this article serious will look at how to migrate our Object Oriented Spring applications into OfficeFloor to start using OfficeFloor's Functional Programming and Threading extensions.  While objects link data with functionality (methods), objects still provide little in terms of threading.  This article series will show how to further simplify our Spring applications with these Functional Programming and Threading extensions/plugins by migrating to OfficeFloor.