The Client API
For action developers who use the framework, we've set aside the shocks.client package. Its job is to abstract component developers from the internal complexities of the framework. At present, this package has three items in it. Figure 2 shows this design:

Figure 2. Arrangement of the shocks.client package
Shocks wouldn't be much of a framework if there weren't points of connection. Actions must have some way of getting at the internal state of the application at runtime. However, reducing the degree to which actions are dependent (or even aware) of the framework is one of our top priorities. People don't want their classes to be locked into a single framework — and who can blame them for that?
In the most absolute basic implementation, actions would only need to implement the Action interface. But the goal that prompted me to develop the framework in the first place was the ability to trade actions between classloaders using JMX. To this end, the framework creates an instance pool in which actions are stored and managed. In addition to cloning the action instances for pooling, the framework sorts the instances by name and version. Toward this end, the CachedAction interface extends Action and Cloneable, and also introduces two methods for getting and setting the action metadata.
The repository that manages CachedAction instances is available through JMX and universally accessible from any classloader in the VM. This allows us to trade actions between WAR files and provide tools for managing the actions (fulfilling the manual override requirement from above), and allows application developers to revert to a previous action versions at runtime. If a new version of an action brings negative side effects to the system, the manual override should allow a developer or admin to roll the system back to an earlier, more stable version. But that sort of functionality comes at a price — action developers must abide by the CachedAction interface or be willing to write a new repository mechanism with fewer interface requirements. Another great technique would be to implement a "dynamic action" that can map to a plain-old Java object (POJO). Of course, this could have negative effects on resources if the POJOs aren't pooled. Regardless, neither solution would be difficult to incorporate into the total system.
The clients package also contains my first feint toward a protocol-neutral state subsystem. Earlier versions passed the HttpServletRequest, HttpServletResponse, and ServletContext directly into the action object through its execute() method signature. In this implementation, the incoming State parameter can be cast to an EnvironmentContext, which provides access to commonly used Internet protocols. It certainly isn't the best way to do business, but it's good enough until we can write a better system.
A better system would aggregate all of the data in the RuntimeState (which the framework uses internally, and which presently implements EnvironmentContext) in a protocol-neutral manner, re-organize it all, and make it accessible to internal and external component developers. This is where the representation subsystem will come in. In other words, the role of Representation components is to manage the interaction between developers and the application state. At that point, a more sophisticated state subsystem can be built without adding complexity for component developers.
There is another area in which the state subsystem is currently lacking. Families of actions (or workflow components, for that matter) can be coupled through their reliance upon values cached in the RuntimeState. This is called "state-coupling," and it can be broken during development if one of the objects alters the key by which they refer to that mutually required value. When this occurs, it can result in runtime errors that are very difficult to track down and debug at compile time. Externalizing the application state and making it manageable could help with this, as could aggressive system-level unit testing. I'm laying out plans for a project to provide this capability.
The client package is completed by the CachedActionSupport object, which provides a bare-bones implementation of CachedAction, and handles the downcasting of State to EnvironmentContext so you don't have to. But you're certainly not required to use CachedActionSupport. You can easily provide your own implementation of CachedAction if you like, or write a new action repository mechanism (as mentioned above) if you don't.
Framework Architecture
The structure of the framework itself is very straightforward, as illustrated
by Figure 3.

Figure 3. Arrangement of the shocks.framework package
The main entry point into the framework is the ShocksHttpServlet. Any sort of handler (like an SMTP handler or a SOAP handler) could also provide entry into the workflow system — this is just the default implementation. When initialized by the servlet container, the ShocksHttpServlet triggers a loading mechanism that parses the shocks-workflow.xml file in the WAR file's WEB-INF directory. All of the XML metadata from that file is loaded into a central repository, which binds it into MDBeans and manages the details of instance pooling. The loading mechanism then uses these MDBean objects to load Action and Filter objects, and combinations of the three to build Sequence objects.
At runtime, the ShocksHttpServlet pulls the command URI from the incoming request object, then packages it (along with the relevant protocol objects) into a new RuntimeState object. That RuntimeState object gets passed on to the WorkflowController, which figures out what to do with it.
The WorkflowController's job is made especially simple because of the way we pre-build and cache metadata and workflow sequences. It passes the command URI to a MetadataRepository and gets a MDBean containing all of the information about that command. The WorkflowController then passes the MDBean as a key to a SequenceRepository, and gets an appropriate Sequence object, which is an ordered set of workflow components. The WorkflowController can then forward to the Sequence, which is a series of filters and a target action. Eventually, the flow returns to the WorkflowController. When this happens, it puts the Sequence back into the repository, examines the RuntimeState, and decides what to do next.
Watching the framework hit the action and come back the first time was like watching a skateboarding legend like Tony Alva or Jay Adams hit the lip of a swimming pool. There and back is just the price of entry. The tricks you pull off while you're getting there and back, and your style — the way you pull those tricks off — that's what really matters.
As noted before, none of these tricks works quite the way I'd like, and a lot of the decision making could be farmed out to a rules engine like Drools. But this proves that the concept and the process are sound, that division of the entire system into a family of explicit roles can be accomplished.
Framework Checklist
Now, after working through this design, how well does it address the five challenges set forth by Dr. Reenskaug? I'm going to grade our progress in this, but not on a curve compared to other projects.
Manual override: B
This is accomplished by making the workflow and action subsystems completely manageable through JMX. At present, it should be possible to override and direct the behavior of an application at runtime. When we plug the Drools engine in and write management hooks for that, we'll have even more fine-tuned control over the application workflow. The addition of state- and representation-management capabilities, as well as some really good UI tools, would drive the grade straight into "A" territory. Look for advances in those areas and a web-based application management suite sometime in the next year.
Modular subsystems: B
The total system has successfully been divided into modular subsystems, but the decoupling could be improved upon. A better representation subsystem could lead to much more developer-friendly environment, and we'll be working in this direction.
Behavioral transparency: B-
This is accomplished through the use of interfaces and access to a common RuntimeState. There is a certain degree of state-coupling, because objects are tied to one another through the values stored therein. If one developer switches the name of an attribute in one object, it could wreak havoc across the entire system. Aggressive unit testing and an external state-management subsystem could simultaneously reduce complexity and raise transparency here (I got the idea from the Apache Cocoon list — it is by no means a novel concept).
Pluggable modules: B+
New features and functionality can be plugged into the framework as WAR files packed with useful Action objects. Entire feature platforms that provide common functionality like form validation, type conversion, and JSF-style command processing can all be added to extend the core framework's functionality. The addition of the Drools workflow system will allow new Petri nets, or "workflow modules" to be plugged into the framework, as well. All of the current instance-pooling mechanisms are replaceable, but this process could be made even easier so that it requires no manipulation of framework class files at all.
Continuous growth: B
By reducing (and aiming to eliminate) the need for third-party subsystem developers to directly manipulate the framework source code, the framework allows the total system to grow with less friction. The core developers of the framework (no matter how good their intentions) represent a bottleneck that prohibits the introduction of functionality to the total system. We are actively working to reduce the need for framework developers to integrate changes into the total system. Eventually, this framework plus its third-party "feature platform" extensions plus your business logic extensions (all three comprise the total system) will be capable of continuous growth with a bare minimum of friction. The ability to break the application down into explicit subsystems is absolutely integral for this to even be possible.
Conclusion
Shocks is clearly not perfect, but that's alright. I just assumed I'd do everything wrong and that eventually every subsystem would need to be torn out and replaced with a better piece — most likely by a better developer. The challenge was to make that process easier, and also to explore new possibilities and expose myself to unexpected consequences. I think I've done that. There are no hard and fast rules in this game, and any absolute principles that might apply have not been identified. You can write something to cover the 80% use case, but that other 20% must be left to artists. If you give them hooks for incorporating their ideas into the whole, hopefully their jobs will be easier and everyone benefits.
The best we can hope for as designers is that every day we render obsolete the technology of the day before, or that we strike upon some guiding principles to help us along our way. With one another's help, I hope we continue doing just that. The tools we use in five years will not resemble the tools we use today. Hopefully, we can plug into each other's talents with better frameworks, and hopefully, this architectural style can help us more easily see how all the pieces fit together.
If not, that's alright too. We can always refactor and hone the style. Right?
N. Alex Rupp studies theoretical geometry and classical literature and is conducting ongoing research in declarative programming.