A grid of vertical businesses and horizontal capabilities surrounding a stable core of business rules

Architecture in Practice · 2026

Accelerating Business Innovation:
Lattice and Clean Architecture in Practice

From extension-point overload to business isolation, reusable use cases, and composable rules

42 min read Published March 15, 2026 Updated August 1, 2026

Abstract

When I joined Company A in early 2015, its transaction platform was already living with a contradiction. The system had been divided into ever smaller services, and the platform exposed more APIs and extension points than ever, yet delivering a business change had not become easier. Teams were short-handed, schedules were long, and delays were common. Every Thursday, dozens of branches converged on the fixed release window, and a change for one business could force many unrelated businesses through another round of regression testing.

The deeper problem was not merely an excess of if/else, nor an SPI mechanism that lacked flexibility. The platform could not answer a more important set of questions: Who owns a customization, and whom should it affect? How does one business scenario remain whole across many applications? How should several rules compose when they apply at once? Can business code evolve and ship without requiring a platform release? Lattice emerged as an open-source business extension framework built around those questions. It separates business code from platform code, one business from another, and logical business architecture from physical application topology. Business identity, Ability, Use Case, vertical and horizontal composition, and Reduce then turn “modify the platform everywhere” into “assemble a business within an explicit boundary.” The essay closes by showing how I turned those judgments into an architecture Skill that reviews design, module placement, and generated code during AI-assisted development.

The decisive quality of a highly customizable platform is not the number of extension points it exposes. It is whether every change has a clear owner, scope, composition rule, and delivery boundary. Lattice is less about inserting business code than about keeping each business change from spilling across the entire platform.

Part I · When the platform can no longer keep up

01In 2015, the problem I saw was not merely technical

During my first six months at Company A in 2015, I spent a great deal of time watching the transaction platform absorb new businesses. What stood out was not one poorly designed interface, but a system under persistent strain: too few people, punishing overtime, long lead times, and projects that routinely slipped. Platform and business teams worked through a co-development model, but every additional participant made coordination more expensive. Everyone was working hard, and the system contained no shortage of sophisticated mechanisms. Still, each new business became harder to deliver.

Distributed architecture was not itself a mistake. Company A’s early commerce business was relatively homogeneous, and a single system could contain product listing, cart, order creation, and fulfillment. As traffic, businesses, and teams grew, splitting that monolith into cohesive user, product, transaction, and shop systems was both necessary and successful. Each system could make local design decisions, own its requirements, and release independently. For a time, development speed and reliability improved.

Every architectural advantage, however, has a scale at which its costs become visible. As the business expanded across industries and the number of applications and services reached into the thousands, the decomposition that had reduced local complexity began producing a different kind of complexity: one coherent business intent was scattered across many applications and teams.

A single request might require several teams to analyze, design, build, integrate, and release changes in a particular order. Worse, nobody could confidently enumerate every system it touched. The main path was usually visible; edge cases and side paths accumulated over years tended to surface late. Missing one could mean reopening the design, reworking the implementation, and delaying the release again.

We had decoupled applications without reassembling business intent.

A system may be split into a thousand services, but the user still asks for one coherent outcome. The more technical boundaries we create, the more we need a way to restore the unity of business intent.

02From open APIs to business co-development

APIs were the platform’s earliest and most important way of exposing capabilities. Stable service interfaces let business systems reuse product, transaction, marketing, and other functions. APIs described stable behavior well, but they could not anticipate every difference among industries.

As those differences accumulated, Map parameters appeared. Callers could inject context the platform model had never anticipated, but a platform that did not understand those parameters could hardly understand their business meaning. Java SPI came next: the platform defined an interface, and a business team implemented it so that specialized logic could run inside the platform’s execution path. This co-development model made business teams far more productive.

Its limits gradually became just as clear. The platform did not provide a strong separation between business and platform code, so both continued to accumulate in the same repositories. A customization could not ship independently; it had to join the shared release train. I remember the fixed release window every Thursday, when forty or fifty branches had to be merged. Each branch made sense in isolation. Together they created an enormous system-wide regression effort: when Business A went live, Business B had to wonder whether its behavior had changed too.

Isolation and quality depended mainly on developers being careful rather than on a structure that could be verified. Experience and code review may be enough when the organization is small. At scale, “everyone should be more careful” is not an architecture.

03How an inventory-decrement method decays

Inventory decrement is a small but representative example. The following code is illustrative and is unrelated to any production source.

The platform initially reduced stock when an order was placed. Large appliances were expensive and scarce, however, and unpaid orders could lock inventory. That business needed stock to be reduced only after payment:

public ReduceType getInventoryReducePolicy(OrderLine orderLine) {
if (orderLine.getProduct().hasTag(9527)) {
return ReduceType.AFTER_PAYMENT;
}
return ReduceType.BEFORE_PAYMENT;
}

When digital goods arrived, some required no inventory decrement at all, so another condition appeared before the first. Large appliances later gained in-store pickup: customers received a digital collection code, yet the physical inventory still had to be decremented. “Digital goods do not decrement inventory” now intersected with “large appliances decrement inventory after payment,” and evaluation order began changing outcomes.

Eventually, large appliances were no longer uniform either. Items above a price threshold reduced stock after payment; cheaper ones still reduced it at order placement. A simple default policy had become a branching tree over product type, industry tag, delivery mode, and price.

No single bad programmer created this decay. On the contrary, every change answered a real request, and every local condition may have been the cheapest reasonable choice at the time. Years later, a method could span thousands of lines. The next developer would search cautiously for a plausible insertion point, add another branch, and hope they had not changed another business’s behavior.

This history changed how I understand legacy code. Often it suffers not from a lack of refactoring, but from a missing ownership model for business change. The code does not know which rule belongs to which business, nor whether two rules should be isolated, overridden, or combined. Execution order ends up making an organizational decision.

04SPI made the code cleaner, not the businesses independent

The natural response is to extract those branches into SPI implementations. Each implementation supplies a filter and an execute method; the platform iterates over the list and runs the first matching strategy.

public interface InventoryReducePolicySpi {
boolean filter(ReducePolicyRequest request);
ReduceType execute(ReducePolicyRequest request);
}

With a small number of implementations, this works well. A thousand-line method becomes several more cohesive classes, improving local readability and testability. At larger scale, however, the same difficulty reappears in another form:

In harder cases, a filter calls a rule engine or remote service. Reading the code is then insufficient to determine statically when an implementation will apply. I have seen complex platforms accumulate nearly a thousand SPI contracts, with dozens of implementations behind a single interface. Global traversal plus “first match wins” turns every business into a participant in one invisible decision tree.

The platform appears to have removed if/else; in reality, the conditions moved into registration order and filter logic. The coupling remains and is harder to see.

  • Symptom — Core methods accumulate branches, and nobody can state the impact of a change with confidence.
  • Initial fix — Extract branches into SPI implementations, selected by list order and filter.
  • New failure — Conditions and ordering form a global decision tree in which businesses still compete.
  • Root cause — The extension model has no business identity, scope, composition semantics, or independent delivery boundary.
Many applications, business branches, and release paths tangled around a shared platform, allowing local changes to spread through invisible dependencies

Figure 1 · An extension mechanism without scope. Service decomposition reduced the complexity of each application, but did not preserve coherent business intent. Global SPIs, shared repositories, and a common release window coupled those scattered changes together again.

05An application view is not a business view

A subtler problem was that the platform exposed extension points by application, while business teams reasoned in scenarios.

At Company A’s scale, business capabilities had already been divided among more than 2,000 microservices. The number mattered not as a measure of prestige, but because the system landscape had grown beyond what any one person could remember, assess, or control as a whole. No individual could reconstruct the complete dependency graph from experience alone. At the start of a request, nobody could confidently say which applications it would traverse, which teams it would involve, which extension points it would change, or which entrances outside the main path might be missed.

This does not mean that one requirement modified 2,000 services, nor that microservice decomposition was itself a mistake. The problem was that after one business intent had been scattered across so many technical boundaries, impact analysis still depended on people discovering and reconnecting the pieces by hand. Analysis became repeated rounds of assembling teams, tracing paths, and confirming assumptions. Missing one system or side entrance could invalidate the result.

Suppose one business does not allow products to be bought through a shopping cart. In business language, there is one rule: “This business does not support cart purchases.” In an application view, the team may need a string of scattered customizations: hide the Add to Cart button on the product page; reject API calls that bypass the page; prevent the order service from accepting requests falsely marked as cart orders; and repeat the same check at every side entrance.

Each application may offer a perfectly reasonable extension point in isolation, but responsibility for completeness has been transferred to the delivery team. Developers must understand not only their industry, but also which small subset of more than 2,000 microservices matters to the scenario, what each service exposes, how the services interact, and whether any critical entrance has been overlooked. Finer decomposition clarifies the responsibility of each system while making end-to-end business analysis harder.

The real challenge, then, was not to invent a stronger SPI. It was to move the organizing unit of extension upward—from applications and technical processing points back to businesses and scenarios. The platform should let developers see the outcome of a complete business scenario first, then let that scenario organize the capabilities it needs. Every delivery team should not have to rediscover the application map from scratch.

Part II · Define the problem before designing the framework

06Six questions the architecture had to answer

During several design workshops, I asked the team not to begin with class diagrams or framework choices. Instead, we completed one sentence: “If the platform could …, then we could ….” This kept us anchored in actual friction rather than technical preference.

The answers were concrete. A team should be able to package the contracting and fulfillment model of a particular business, then keep that package independent of both the platform and other businesses. It should be able to select and compose existing capabilities along real business flows. Product managers and developers should be able to see both what the platform offers and what a business has adopted. No business should have to wait for a shared platform release, and the same logical architecture should work across different deployment models.

Condensed further, those wishes became six questions the framework had to answer explicitly:

From platform symptoms to architectural questions
Platform symptom Question the architecture must answer
Business code mixed into platform repositories Who owns the change, and where is its code boundary?
A change for Business A can alter Business B How is the scope of a rule determined?
One requirement is scattered across applications How can capabilities be organized and reused as a complete scenario?
Extension points are buried inside physical applications How can the platform expose one business view independent of deployment topology?
Several SPI implementations match at once How do rules compose, and who resolves a conflict?
Dozens of branches share one release Can a business be developed, tested, and delivered independently?

These six questions determined the shape of Lattice. Its annotations, interfaces, and runtime are implementation devices. Forget the questions and Lattice can easily be reduced to yet another SPI toolkit.

07Rereading Clean Architecture: the circles are not the point

When I returned to Robert C. Martin’s Clean Architecture, its most useful lesson was not which project directory should contain Entities, Use Cases, Interface Adapters, and Frameworks. It was the direction in which change is allowed to travel.

The book’s best-known principle is the Dependency Rule: source-code dependencies point inward. The outer layers contain volatile mechanisms; the inner layers hold more stable policies closer to business purpose. Web frameworks, databases, and delivery mechanisms serve business rules, not the other way around. A use case coordinates entities toward an application goal and exchanges data with external systems through boundaries, while dependencies across those boundaries still point inward. [1][2]

For a large business platform, however, making a database depend on a domain interface is not enough. Business customization is itself an external source of change. The inventory policy of one industry, the purchasing limit of one promotion, or the redemption requirements of one fulfillment model should not leak back into the core shared by every business.

That led me to a more concrete reading of the Dependency Rule. Architectural “inside” and “outside” are separated not only by technical layer but also by policy scope. Shared entity rules, scenario rules, vertical-industry rules, and horizontal capability rules differ in stability and reach. They need explicit boundaries through which to collaborate, rather than a single global list in which they all compete.

Clean Architecture also emphasizes independent development and deployment, plugin boundaries, and an architecture that reveals the system’s purpose before the frameworks it uses. [1] Those ideas gave me better language for conclusions that practice had already forced upon us:

What I took from the book: Clean does not mean fewer lines, more layers, or more interfaces. It means that high-level business policy is not dragged around by low-level mechanisms and local customization. When change arrives, we know the boundary at which it should stop.

Part III · How Lattice answers the problems

08Why the name Lattice

A lattice is a grid. When we looked at a business platform, we saw two interwoven dimensions: isolated vertical businesses and horizontal capabilities that could be selected and overlaid. Their intersections looked like a lattice. There was also a less solemn reason: programmers are famously fond of plaid shirts.

That is why we named the business extension framework Lattice. It was not an attempt to give conventional SPI a new label, but to make business identity and capability composition first-class concepts. The source is available at hiforce/lattice; at this essay’s most recent update, the main branch version was 1.0.21. [3]

09Lattice begins with three separations

The failures described above look varied, but all three fundamental boundaries had collapsed. Business code had entered the platform core; implementations for different businesses competed in one global decision space; and physical applications had fragmented the view of an end-to-end business process. Lattice does not begin by adding more hooks. It begins by restoring those three separations.

The three separations in Lattice
Separation What must be kept apart Boundary mechanism Immediate effect
Business from platform Stable platform capabilities and business-specific policy Stable contracts, business plugins, separate repositories Customization stays out of the platform core
Business from business Rules and implementations owned by different businesses Business identity, targeted loading, call-chain context One business’s change does not enter another by default
Logical from physical architecture Business flows and application or service topology Annotation metadata, unified reporting, management view Designers discover capabilities by scenario rather than application map

These are not three unrelated features. The first determines where code belongs. The second determines which rule space a request may enter. The third determines what people see when they design a change. Omit any one and the others weaken: plugins without identity can still match the wrong business; identity without a code boundary still ties every business to the platform build; runtime isolation without a logical view still leaves impact analysis wandering through more than two thousand services.

A platform core connected to external business plugins through contracts, isolated lanes for separate business identities, and one logical business flow above a fragmented physical service topology

Figure 2 · The three separations in Lattice. On the left, business change stays outside the platform core. In the middle, identity selects one isolated rule space before execution. On the right, capabilities scattered across physical applications are restored to a logical view that people can design and assess.

10First separation: business code from platform code

The old co-development model asked the platform to define an SPI and business teams to submit implementations directly into the platform project. It worked well at first because a new business could enter the main flow quickly. At scale, however, stable platform mechanisms and local business policies accumulated in the same repository. Platform changes had to account for business implementations; business changes inherited the platform’s branching, regression, and release cycle. Each side became embedded in the other.

Lattice keeps only stable entities, use cases, abilities, extension contracts, and default behavior in the platform repository. Concrete business rules live in separately owned plugin packages and repositories. Registration and plugin loading connect the two at runtime: the platform declares what may vary, while the plugin declares how one business behaves at that point. Dependencies point toward stable platform contracts; the platform core does not depend on a particular business. [3]

Three boundaries can then align:

Plugins do not guarantee independent delivery. Shared data, contract compatibility, runtime containers, and organizational process can still couple releases. They do, however, remove the most dangerous reverse dependency from the source tree and make independent build, verification, and delivery an option the architecture genuinely supports.

11Second separation: one business from another

A conventional SPI asks, “Which implementation’s filter matches this request?” Lattice first asks, “Which business does this request belong to?”

The questions sound similar but produce very different systems. In a conventional SPI, every implementation uses a filter to guess whether it applies to the current request, so implementations owned by every business share one candidate set. Miss one constraint, cover one channel too broadly, or let two conditions overlap, and a rule written for Business A may execute for Business B. Correctness is distributed across every implementation; as businesses and combinations grow, proving that all filters remain mutually exclusive becomes unrealistic.

Lattice reverses the order: the first action at every business-scenario entry point is to establish business identity. Once identity is known, the runtime has an explicit coordinate. It loads only the plugin packages and realizations associated with that identity; code owned by other businesses never enters the candidate set. Isolation is no longer a fragile convention that every developer must preserve in every filter. It becomes part of the execution model.

@Business(code = "business.a", name = "Business A")
public class BusinessA extends BusinessTemplate {
}

@Realization(codes = "business.a")
public class BusinessAExt extends BlankOrderLinePriceExt {
@Override
public Long getCustomUnitPrice(OrderLine orderLine) {
return 2000L;
}
}

@Business declares the business; @Realization declares which business owns the implementation. The call chain carries a unified business code, and the runtime locates plugins and realizations by that identity. The annotation syntax is incidental. The important shift is from ownership inferred through conditions to ownership declared explicitly: establish who I am first, then discover which rules are mine.

The identity must survive the entire call chain. If an entry point knows the business but loses that identity when it calls a downstream service, isolation ends at the process boundary. Lattice treats the business code as a core piece of extension context, keeping one business’s rules coherent across capability calls.

The first two separations now establish code ownership and runtime scope. The third requires a vocabulary that can lift platform capabilities out of scattered interfaces and organize them into complete use cases. Ability, the two business axes, Reduce, and Use Case provide the language from which that logical business view can be built.

12Ability makes platform capabilities discoverable and callable

Business identity answers “whose rule?” An Ability answers “what can the platform do for this business object?”

In Lattice, an ability groups extensible behavior around a particular business object. An order line may be that object, pricing the ability, and “custom item unit price” one extension point. The ability loads realizations for the current business and combines their results according to explicit reduction semantics; the business implements only the behavior it needs to change.

public interface OrderLinePriceExt extends IBusinessExt {
String EXT_ORDER_LINE_CUSTOM_UNIT_PRICE =
"OrderLinePriceExt.EXT_ORDER_LINE_CUSTOM_UNIT_PRICE";

@Extension(
code = EXT_ORDER_LINE_CUSTOM_UNIT_PRICE,
name = "Custom the Item's unit price of OrderLine",
reduceType = ReduceType.FIRST
)
Long getCustomUnitPrice(OrderLine orderLine);
}

@Ability(name = "OrderLine's Price Ability")
public class OrderLinePriceAbility
extends BaseLatticeAbility<OrderLinePriceExt> {

public OrderLinePriceAbility(OrderLine orderLine) {
super(orderLine);
}

public Long getCustomUnitPrice(OrderLine orderLine) {
return Optional.ofNullable(reduceExecute(
ext -> ext.getCustomUnitPrice(orderLine),
Reducers.firstOf(Objects::nonNull)))
.orElse(orderLine.getUnitPrice());
}

@Override
public BlankOrderLinePriceExt getDefaultRealization() {
return new BlankOrderLinePriceExt();
}
}

This changes three things. First, platform capabilities become discoverable; developers no longer search thousands of applications for scattered SPIs. Second, the platform still supplies default behavior, so a business with no customization falls back naturally. Third, the meaning of multiple results is defined by the ability rather than by accidental registration order.

Restraint matters here. Ability is not a new word for wrapping every method. Only behavior with stable business meaning—and for which different businesses genuinely need different choices—deserves to become an ability or extension point. More interfaces do not automatically create a more flexible platform. They may simply create another surface that must be maintained.

13Before the two axes, what counts as a business?

“Business” is one of those words that can expand to mean almost anything. People often call whatever they happen to own “the business,” or equate a business with a box on the organization chart. For the model to be useful in code, I need a more operational definition: a business is a sustained set of activities, decisions, and rules that delivers a product or service and produces an economic outcome through revenue growth or lower cost.

That definition includes both sides of the ledger. Companies often organize teams as profit centers and cost centers. Profit centers focus more heavily on markets, products, operating models, and supply chains that create revenue. Cost centers focus more heavily on shared platforms, process improvement, and efficiency. Their immediate concerns differ, but both ultimately answer the same operating question: how can the company keep creating value while preserving healthy room between revenue and cost?

On a software platform, those operating changes consistently produce two kinds of rules. One kind usually belongs to a particular industry and describes how that industry prices, transacts, fulfills, and manages risk. Together, those rules form a coherent operating model. The other kind comes from an industry-agnostic play or platform capability. It changes one part of the transaction in a way that can add revenue, efficiency, or customer value across many industries. The first kind needs autonomy; the second needs reuse. One answers “which industry’s rules are these?” The other answers “which reusable plays has this industry chosen?”

That is where the vertical and horizontal axes come from. They were not invented to make an architecture diagram look tidy. They are the rule-level projection of two realities: who owns an operating outcome, and how a platform reuses capabilities across owners.

14Why business rules fall along vertical and horizontal axes

A vertical business is usually an industry. It is a business space with end-to-end operating responsibility: auto finance, on-demand delivery, or movie tickets. It owns more than a few configuration differences. It is accountable for the industry’s customers, revenue, fulfillment, risk, and final operating result. Its rules therefore need isolation. Auto finance rules for qualification, preauthorization timeout, and non-logistics fulfillment should not leak into on-demand delivery. Delivery pricing, service radius, and SLA rules should not become defaults for movie tickets.

Even when two vertical businesses happen to use the same ten-minute payment timeout today, each should declare that value in its own business space. They may reuse the payment-timeout capability and its implementation, but they should not share one business’s ten-minute decision. If one changes the value to fifteen minutes tomorrow, the other should not change without taking part in that decision. The vertical axis establishes ownership and isolation: similar code does not turn one operator’s policy into another operator’s policy.

In the Lattice model, a horizontal business is usually an industry-agnostic play or platform. We might also call it a platform business or cross-cutting capability. It does not own a complete industry operating model. Instead, it packages a reusable way of creating value and modifies a slice of the vertical baseline. The core of electronic vouchers is code issuance, redemption, and refunds; it does not matter whether the adopter sells movie tickets, attraction tickets, or car-care services. A campaign may change the purchase entry point, price, and sales-counting rules across industries. Cash on delivery changes when payment occurs, how checkout behaves, and how small cash amounts are rounded, regardless of the vertical.

A horizontal business is defined independently of any one vertical, but many verticals may choose to adopt it. Movie tickets and car-care services can both use electronic vouchers while choosing different redemption counts. One business can install vouchers and presales together, then add a campaign as well. A vertical adopts those changes because a horizontal capability can expand what is sellable, improve conversion, lower cost, or improve the customer experience. The horizontal axis provides reuse and controlled variation: it does not replace the vertical business, but supplies optional changes to its baseline.

The two kinds of business also differ in a way that matters enormously to the architecture: vertical industries are relatively enumerable and stable; horizontal plays are open-ended, effectively impossible to enumerate in advance, and change frequently. I do not mean “countable” and “uncountable” in the set-theoretic sense. This is an engineering distinction. A company can usually list the industries it operates today. Entering a new industry is an explicit, relatively infrequent strategic decision, so an industry can carry a durable business identity. There is no equivalent final list of horizontal plays. New campaign mechanics, fulfillment methods, payment options, membership benefits, and risk controls keep appearing—and keep combining with one another.

If every “industry × set of horizontal plays” became a new business type, then with |V| verticals and |H| independently selectable horizontal capabilities, the theoretical number of combinations would approach |V| × 2^|H|. Each new horizontal capability could double the combination space. Encoding those combinations as subclasses, business codes, or global SPI conditions would make the type system and the decision tree explode together.

Lattice models the problem in the opposite direction. A relatively small, stable set of vertical industries provides the coordinate system and the business identities. Each identity then selects N horizontal businesses from an open-ended set. Each request is first anchored to one vertical identity, acquiring that vertical’s defaults and ownership boundary. The runtime then resolves the horizontal capabilities and realizations selected for that identity. Adding a horizontal play is like adding a new optional column; it does not require redefining every vertical. Changing one vertical’s selection does not rewrite another vertical’s baseline. A stable identity carries an open-ended composition, which is what keeps high-frequency customization from destabilizing the platform.

Two boundaries follow. A workflow that crosses industries should coordinate several explicitly identified business contexts, not make one request belong to several vertical spaces at once. And within each vertical baseline, the platform may select zero, one, or many horizontal capabilities. N may be zero, and it may keep growing as the business innovates.

The effective rule set for a business can therefore be expressed as:

Complete business rules = 1 vertical baseline + N horizontal increments (N ≥ 0)

The “1” establishes a single, stable owner and a set of defaults. The “N” represents open-ended, reusable, composable enhancements. This is not class inheritance, nor is it a blind concatenation of configuration files. It is a sparse, scoped composition—each vertical selects only the horizontal capabilities it needs—with explicit ordering and reduction semantics. Conventional SPI offers one flat global list of implementations. Lattice first resolves the vertical business space, then the horizontal capabilities selected in that space, and finally the way their rules combine at each extension point.

Isolated vertical businesses intersect reusable horizontal capabilities, with each intersection representing an explicit selection or customization

Figure 3 · The two business dimensions in Lattice. Vertical businesses preserve ownership and isolation; horizontal capabilities provide reuse across industries. An intersection is not a global condition that happened to match, but an explicit choice by one vertical business to adopt one horizontal capability.

15Reduce does not make the policy decision; it makes conflict explicit

Once the complete rule set is defined as one vertical baseline plus N horizontal increments, conflict is no longer an edge case. It is a question the composition model must answer directly. Several horizontal capabilities may modify the same business decision, and the vertical baseline may already have a policy of its own. The framework must do more than find those rules: it must state whether they can coexist and, if they can, how they produce one result.

Some rules are reducible. Consider purchase quantity. The vertical business says it cannot exceed available stock. An electronic-voucher capability allows at most 250 units per purchase. A campaign limits the order to 10. These are upper bounds from different sources, not attempts to override one another. Taking the minimum satisfies all three, so a reducer such as MIN has clear business meaning.

Other rules are mutually exclusive. A campaign may exclude a sale from volume statistics by default, while a particular vertical business—after agreement between the two owners—requires its own sales-counting policy to prevail. Two Boolean answers cannot be reconciled by taking a minimum or merging a collection. The composition needs an explicit priority rule stating whether the vertical or horizontal policy has final authority.

Lattice therefore treats composition as two separate questions: a Reduce algorithm answers “how can several results be combined?”; conflict priority answers “who decides when they cannot be combined?” A reducer may select the first non-null value, merge collections, take a minimum, or apply another algorithm with clear business semantics. Priority must be agreed and declared by the relevant business owners. A framework cannot make that policy decision for them, but it can require the decision to be explicit instead of hiding it in registration order.

Effective business rule = Reduce(vertical baseline, horizontal increments 1…N, conflict priority)

From conventional SPI to Lattice extension invocation
Dimension Conventional SPI Lattice
Ownership Inferred indirectly through filter Declared through business identity
Candidate scope Global implementation list Current business and its selected capabilities
Multiple matches Registration order or first match Reduce and explicit conflict priority
Business reuse Copy implementations or share hidden conditions Compose horizontal capabilities by business
Impact analysis Read every filter Inspect the business space and composition

16Use cases let architecture speak business language again

Abilities organize extensible behavior, but business stakeholders rarely frame a problem as a method on an entity. They talk about scenarios such as “buyer places order,” “seller fulfills,” or “buyer requests refund.” Clean Architecture places use cases outside entities: a use case guides data through entities and coordinates them toward a particular application goal. [2]

That distinction matters in a transaction platform. An electronic-voucher scenario spans orders, voucher issuance, notifications, redemption, and several systems. Entity-level SPIs alone still leave the business team to assemble the end-to-end process. Lattice therefore supports reusable assets at the use-case layer and scenario SDKs organized around business activities.

For electronic vouchers, the platform can provide the end-to-end capabilities for generating, issuing, redeeming, and refunding vouchers, while opening only policies where real differences exist:

public interface ETicketTradeSDK extends IBusinessExt {
@Extension(reduceType = ReduceType.NONE)
Boolean isVoucherSupportMultiWriteOff();
}

A movie-ticket business can allow a single redemption; a car-care package containing twelve washes can allow multiple redemptions. The business developer sees the policy “does this voucher support multiple redemptions?” They do not need to know which K/V attribute an order uses to store the answer, or search each application for a technical hook.

The scenario layer maps a meaningful rule onto lower-level entities. During order persistence, for example, the voucher use case can translate “multiple redemption allowed” into an order attribute. The entity knows how to save attributes reliably; it need not know about movie tickets, wash packages, or a future industry. The scenario depends on the entity, while the entity does not depend on a specific scenario—an instance of the Dependency Rule applied to business extension.

Most importantly, scenario assets can compose. A movie-ticket business can install both electronic voucher and presale capabilities: vouchers decide redemption behavior, while presale determines the deposit ratio. The business does not inherit an ever-growing “movie ticket platform.” It installs two relatively independent scenario capabilities inside its own business space.

17Third separation: logical business architecture from physical application architecture

The first two separations determine where code belongs and which rule space a request enters. Ability and Use Case then provide a vocabulary for capabilities and complete scenarios. On that foundation, the third separation determines how people understand the platform. Traditional platforms expose extensions through physical applications: the product service has one set of SPIs, while cart, transaction, and fulfillment services each expose another. That catalog faithfully describes deployment, but it does not explain how a business scenario works end to end. Once the estate exceeds two thousand microservices, asking any person to master the application topology before designing a business change is asking for impossible impact analysis.

Lattice uses annotations such as @Ability and @Extension to declare capabilities, extension points, defaults, and reduction semantics. @Business and @Realization declare businesses and implementation ownership. A runtime can discover this metadata and report it into a common business-management plane. The resulting organizing unit is no longer “a Java interface in application X.” It is a business flow—ordering, fulfillment, refunds, redemption—and the business policies that may vary along that flow.

Business stakeholders and designers can therefore begin with the logical view. They can follow an end-to-end process, see which capabilities the platform already provides, which horizontal plays the current business has installed, which extension points already have realizations, and which Reduce policy governs a collision. Existing pieces can be selected and composed. Anything that still appears to be missing becomes a concrete gap list for discussion with platform designers. The conversation changes from “I think some system may need a change” to “this business step lacks a reusable policy.” Platform evolution becomes more deliberate as a result.

The management view must be more than a flat extension registry. To support design and impact analysis, each extension point needs business meaning, scenario placement, inputs and outputs, default behavior, Reduce policy, owner, compatibility information, and current adopters. The platform should also preserve traceability from each logical node to its physical applications, interfaces, and runtime instances. Separating logical from physical architecture does not hide the physical system. It stops making deployment topology the starting point for business design.

Annotations do not create a sound business model by themselves. Reporting every low-level method would merely replace a map of two thousand services with an equally unmanageable interface catalog. Platform designers must still identify stable business semantics, organize use cases and abilities, and own contract evolution. Lattice provides a loop from code declaration to unified reporting, business design, and runtime execution; it does not replace modeling.

This also reflects my reading of “frameworks are details.” Lattice belongs in its proper place. It resolves identity, invokes extensions, loads realizations, discovers metadata, and reduces results, but the business rules that matter still belong to businesses and use cases.

Stable entities and use cases at the center, with business plugins and external infrastructure beyond explicit boundaries and all dependencies pointing inward

Figure 4 · Where Lattice sits in a clean architecture. Lattice lets external business plugins participate through stable contracts while dependencies still point toward entities and use cases. The framework, database, and plugins are mechanisms; none should become the center of business policy.

18Do not make the runtime rediscover decisions we can make up front

Much of the risk in conventional SPI comes from dynamic guessing: iterate over every implementation and run an arbitrary filter until something matches. The answer may depend on current data, a remote call, or implementation order, making the blast radius impossible to state in advance.

Lattice favors determining which capabilities a business uses, how rules compose, and how conflicts are resolved during management and configuration. Runtime then executes the established rule set and conflict policies. The official repository likewise identifies separation of management and runtime domains as a core design. [3]

This does not mean runtime behavior has no dynamic conditions. It means distinguishing two kinds of information. Which rules belong to this business should be as static, visible, and auditable as possible. What those rules do with this particular order depends on runtime data. The former should not be rediscovered on every request.

Once composition can be inspected before execution, product managers and developers can perform impact analysis together: which scenarios are installed, which extension points they cover, how many realizations target the same point, and which Reduce policy applies. Architecture no longer exists only in a code reader’s head; it becomes a manageable business model.

Part IV · A minimal example, then a complete composition

19Business A customizes price; Business B keeps the default

The smallest end-to-end Lattice example is straightforward. The platform defines an extension for customizing an order line’s unit price, along with a pricing ability. Business A declares its identity and implements that extension to return 2000L; Business B makes no customization. Both orders start with a default unit price of 1000L.

OrderLine orderLine = new OrderLine();
orderLine.setUnitPrice(1000L);
orderLine.setBizCode("business.a");

OrderLinePriceAbility ability = new OrderLinePriceAbility(orderLine);
Long unitPrice = ability.getCustomUnitPrice(orderLine);

The result for Business A is 2000L; Business B still receives 1000L. Superficially this resembles the Strategy pattern. The difference is that business identity locates the realization, the ability owns the default, and the extension contract defines result reduction. The complete Quick Start and more realistic compositions are available in lattice-sample. [4]

At this essay’s latest update, the sample repository’s main branch still pinned Lattice at 1.0.19. I temporarily changed it to 1.0.21 and rebuilt both the Quick Start and Use Case modules; both passed. The console output in the next section also came from an actual 1.0.21 run. This is modest verification, but it keeps the code in an architecture essay from being merely plausible.

As the number of realizations grows from two to hundreds or thousands, the value of these extra concepts becomes clear. They restore the information that old implementations lacked: which business owns the change, which capability it extends, how it composes, and what happens when no realization exists.

20Electronic vouchers and presales in one business

Consider a combination closer to a real business. The movie-ticket business installs the electronic-voucher scenario and declares that a voucher may be redeemed only once. It also installs a presale scenario with a 40 percent deposit. The ordering use case invokes both scenario abilities and maps the business data that must be persisted onto the order entity.

[UseCase]PreSaleTrade load custom down payment ratio: .40
[UseCase]ETicketTrade is support multiple write-off: false
Save OrderLine id: 1, bizCode: hi.movie
-- Total Pay Price: 4000
-- Order Attributes: [e_multi_write_off:0]

Four responsibilities remain distinct behind this output. The business plugin declares what differs for movie tickets. The presale and electronic-voucher use cases encapsulate reusable scenarios. The order entity owns stable data and lifecycle. The Lattice runtime connects realizations by business identity and reduces extension results.

If a car-care business is added later, it can reuse the same voucher scenario while allowing multiple redemptions. If it also uses presales, its deposit ratio can be configured independently. The movie-ticket rules do not change, and the new business does not enter a global SPI ordering contest.

21A complete mapping from problems to mechanisms

How Lattice answers each platform challenge
Original challenge Lattice mechanism Immediate change
Business and platform code intertwined Business plugins and separate code boundaries Customization stays outside platform core
Businesses influence one another Unified business identity Realizations are isolated by business space first
Extension points scattered across applications Ability and scenario-level SDKs Extension contracts are expressed in business terms
Business flows fragmented by physical topology Annotation metadata, unified reporting, and a management view Capabilities are discovered by scenario and missing policies become explicit
Horizontal capabilities are hard to reuse Vertical businesses plus horizontal composition Scenario assets can be selected and overlaid
Implementation order determines results Reduce and conflict priority Composition semantics become contractual
Runtime filters hide impact Management decisions separated from execution Compositions can be inspected in advance
One release and system-wide regression Plugin loading and delivery boundaries Independent business delivery becomes possible

This table captures the point I most want readers to retain. A framework should not be evaluated first by the number of annotations and extension strategies it offers. Each mechanism should answer a real problem that appears repeatedly.

Part V · Rereading the book made the limits matter more

22Lattice does not eliminate business complexity

Clean Architecture is easy to misread as a promise that correct layering keeps a system clean by itself. Extension frameworks invite a similar mistake: once code is plugin-based, any business can supposedly customize at no cost. I now prefer the opposite emphasis. Lattice does not remove business complexity; it tries to put each kind of complexity where it belongs.

Whether two vertical businesses should be isolated is a modeling decision. Whether a horizontal capability is genuinely reusable requires evidence from real scenarios. When two rules conflict, an owner still has to decide which wins. If an extension point is too fine-grained or too technical, business teams will still drown in implementation detail. A framework can make decisions explicit, but it cannot make the right decisions for us.

Several misuses deserve particular caution:

Before adding Lattice, I would ask three questions. Is this a stable, nameable point of business variation? Are its owner and scope clear? If two realizations apply, is there an explainable composition rule? If I cannot answer all three, adding another extension point usually postpones the modeling problem rather than solving it.

23Seven engineering judgments I took from rereading Clean Architecture

Years later, I no longer read the book mainly as advice about which directory should contain entities, use cases, and adapters. Circles, module names, and interfaces are representations. The real test is the cost of change: how many unrelated teams a new business must involve, how many existing businesses need regression testing, whether a fault stays inside a known scope, and whether the system preserves the option to replace mechanisms and deployment forms later.

Combined with the Lattice experience, that reading produced seven engineering judgments. They are not a second summary bolted onto the essay. They compress business identity, use cases, Reduce, plugin boundaries, and the management view into one design-review checklist.

01 · Judge architecture by the total cost of change — Layers and interface counts prove little; ask who a change disturbs, what must be retested, and whether a failure can be contained.
02 · Establish scope before extension — State which business owns a rule; do not let a global filter stand in for ownership.
03 · Speak business language before defining technical interfaces — Organize code around use cases and stable policy rather than exposing physical application steps as contracts.
04 · Composition needs semantics — When several realizations apply, Reduce, ordering, and conflict priority belong in the contract.
05 · Give logical boundaries physical support — Repositories, plugins, tests, and delivery should align; independent build and verification are pressure tests for whether a boundary is real.
06 · Begin with business design, and settle management decisions before runtime — Discover and audit capabilities along the flow before tracing them to applications; do not make every request rediscover a composition that could have been declared earlier.
07 · Keep the framework a detail and preserve future options — Lattice serves business rules. Stable policy belongs at the center; mechanisms, plugins, and deployment forms remain replaceable outside it.

The first judgment states what architecture ultimately optimizes. Judgments two through six describe the boundaries through which change should travel. The seventh prevents the solution itself from taking control of the business model. At this point, however, people still have to remember to apply them. In the age of AI-assisted programming, the next step is to turn those judgments into a workflow that a coding agent must execute before it touches the code.

24From the transaction platform to Spark: turn architecture into a Skill for AI coding

The transaction-platform experience taught me why boundaries matter. Spark introduced a more immediate problem. An AI agent can generate changes across several modules in minutes, but it usually sees the current task and nearby files—not the dependency direction, ownership decisions, and expensive failures that shaped the repository over years. “Please follow clean architecture” is far too weak a prompt. It still tends to produce locally reasonable code that crosses system-wide boundaries.

I began organizing architectural knowledge into four layers. docs/principle/01.clean_architect_with_lattice.md preserves the narrative and explains why the boundaries exist. docs/principle/02.ai-clean-architecture-lattice-guidelines.md reduces that history to checkable placement rules, Session Scope rules, and Process / Activity / Result rules. The repository-level AGENTS.md states which kinds of work must load those rules first. An architecture Skill then turns reading, reviewing, designing, coding, and auditing into a workflow the AI is required to follow.

From architectural knowledge to AI coding constraints
Layer Purpose Question it answers
Background principles Preserve cases, reasoning, and trade-offs Why is the system designed this way?
Executable rules State allowed placements, prohibitions, and checks What code may go where?
Repository triggers Require the AI to load specific principles for specific work When is an architecture review mandatory?
Architecture Skill Fix the review sequence and required output What must the AI do before and after coding?

The Skill is not the sole copy of architectural truth. Durable rules stay in the repository so Codex, Claude, and human engineers can all read the same source. The Skill is an execution adapter: it loads those facts at the right moment and prevents the agent from skipping directly to code generation. A shortened version derived from Spark’s actual rules looks like this:

---
name: spark-clean-architecture-review
description: Review Spark backend design, module changes, and Lattice extensions before coding.
---

# Workflow
1. Classify the change as Entity, Use Case, Adapter, or Plugin.
2. Declare modules, packages, and dependency direction; never put DOs, repositories, or plugin clients in an SDK.
3. If the platform path branches on cluster, language, or cloud provider, replace the branch with a stable business identity and plugin realization.
4. Open one Session Scope at the scenario boundary; propagate that identity through Process and Activity code.
5. Give a complex Use Case one Request and one Result; gather side effects at the end of the Process.

# Required output
- Business identity and scope entry point
- Module placement and dependency direction
- Ability / Extension / Reduce design
- Use Case / Process / Activity / Result design
- Existing violations and verification plan

In practice, I do not begin by asking the AI to “implement the feature.” I make the Skill force an architectural answer first. Suppose Spark needs Python builds on Kubernetes. The conversation might look like this:

Me: Add Kubernetes-based Python builds to Spark Flow. Run the architecture Skill first and do not write code yet. Show the business identity, module boundaries, dependency direction, extension contract, and verification plan.

AI: Use the identity build.k8s.python. Stable Context, Result, Extension, Ability, and default behavior belong in spark-console-sdk; orchestration remains in spark-pipeline; the Kubernetes-specific Python implementation belongs in spark-k8s-plugin. The plugin receives source material, build parameters, and credential references prepared by the platform, then returns image, log, and status observations. It does not read platform business tables. The scenario entry point opens one Session Scope, and Pipeline does not add a new PYTHON branch.

Me: Check that design against the dependency rules. If it passes, implement one module at a time and report the governing rule and test evidence after each layer.

AI: I will implement the leaf extension contract and safe default, then the Ability, plugin @Realization, platform invocation, and tests. Before handoff I will check that the platform does not depend on the plugin, the SDK contains no implementation objects, and no internal helper rediscovers business identity.

The module names are not the important part of that exchange. What matters is that the AI exposes its architecture model before generating thousands of lines. Identity, placement, and dependency direction can be corrected while the cost is still low. The same Skill audits the result afterward: Did a Controller quietly acquire orchestration? Did a database object enter the SDK? Did a plugin bypass Context and read a platform table? Did an inner helper open another Session Scope? Did individual Activities write the database and search index on their own?

For complex operations such as creating, transitioning, or bulk-updating tasks, the Skill also requires one Request and one Result before code generation. A Process orchestrates several Activities; each Activity adds validation, authorization, assignment, persistence plans, and event plans to the same Result. Database, index, and event side effects occur together at the end. The AI therefore generates more than a set of classes that happen to run. It generates a use case whose conceptual integrity remains visible.

An architecture Skill reviewing business identity, module boundaries, and dependency direction before guiding an AI to compose one use case around a shared result and controlled side effects

Figure 5 · Architecture principles as executable rules for AI coding. Narrative documentation preserves the reasons, executable guidelines state the constraints, and the Skill produces a design before coding and audits violations afterward. Identity is established at the entrance, Activities share one Result, plugins exchange material through contracts, and side effects gather at the end of the Process.

This changed my view of architecture documentation. It used to explain code after the fact. In AI-assisted development, it is both knowledge and control input. It must carry reasons, permissions, prohibitions, triggers, and evidence of verification. AI does not automatically produce clean architecture, but it gives us an opportunity to turn judgments once passed down by senior engineers into a repeatable review performed on every design and implementation.

Seen this way, Lattice is more than a runtime mechanism for locating an extension implementation. Business identity, Context, Result, Ability, and plugin contracts form a change protocol that people and AI can both understand: who may alter what, where the code belongs, what material crosses the boundary, what evidence comes back, and where the effect should stop.

25Conclusion: let change stop where it belongs

The transaction platform I encountered in 2015 did not lack APIs, services, or extension mechanisms. What it lacked were boundaries: between business and platform, between one business and another, between logical business flows and physical application topology, and between management decisions and runtime execution.

Without those boundaries, adding APIs creates more context parameters, adding SPIs creates a larger global implementation tree, and splitting more applications creates more coordination. Each locally reasonable technology shifts its cost somewhere else as scale grows.

Lattice began by making those displaced and hidden costs explicit in the architecture. Business plugins separate business from platform. Business identity separates one business from another. Annotation reporting and a management view separate logical architecture from physical topology. Ability then organizes extensible behavior; vertical and horizontal dimensions express isolation and reuse; Reduce states how rules compose; and use cases restore the language of complete business scenarios. The three separations establish the boundaries, while the remaining mechanisms make those boundaries designable, executable, and verifiable.

Rereading Clean Architecture has made me more certain that the value lies not in the number of concepts invented. Good architecture does not promise that business will cease to be complex. It lets us say, when a new change arrives, where it belongs, what it depends on, whom it affects, and where it should stop.

Now that AI participates in design and implementation, those boundaries cannot remain private experience held by a few senior engineers. They must exist at once as repository knowledge, executable Skill steps, and verification evidence at handoff—so that faster code generation does not erase the intended direction of change.

That is what “clean” means to me now: not an absence of change, but change that no longer spills everywhere.

References and further reading

  1. Robert C. Martin. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Pearson / Addison-Wesley, 2017.
  2. Robert C. Martin. “The Clean Architecture Dependency Rule.” InformIT, 2017.
  3. HiForce. “Lattice Framework.” GitHub. At this essay’s latest update, the main-branch pom.xml declared version 1.0.21.
  4. HiForce. “Lattice Sample.” GitHub. Includes Quick Start, Use Case, and multi-capability composition samples.
  5. HiForce. “Lattice Clean Architecture Practice Sample.” GitHub. Demonstrates project organization across entities, use cases, interface adapters, and business plugins.