The 60 Apache Struts questions interviewers actually ask, with direct answers, runnable config and code, and what the interviewer is listening for. Grouped for freshers, intermediate, and experienced rounds.
60 questions with answersKey Takeaways
Apache Struts is an open-source Java framework for building web applications on the Model-View-Controller pattern, maintained by the Apache Software Foundation. It sits on top of the servlet and JSP layer and gives request handling a clear structure: a front controller receives the request, routes it to an action, and hands the result to a view. There are two lines. Struts 1, released in 2000, used an ActionServlet, ActionForm beans, and Action classes wired through struts-config.xml. Struts 2, released in 2007, is a ground-up rewrite based on the older WebWork project; it uses plain POJO actions, an interceptor stack for cross-cutting work, OGNL for data access, and the value stack for passing data to views. Most interviews mean Struts 2 when they say Struts. According to the official Apache Struts documentation, the framework's job is to keep the controller, model, and view concerns separate so applications stay maintainable as they grow. In interviews, questions probe the request lifecycle, interceptors, OGNL and the value stack, validation, and the Struts 1 to Struts 2 migration, plus the security history that makes version awareness matter. This page collects the 60 questions that come up most, each with a direct answer and runnable config or code. Increasingly the first backend round is a live AI coding interview, so pair this bank with our AI interview preparation guides for the format side.
Watch: Struts 2 Tutorial 01 - Introduction To MVC
Video: Struts 2 Tutorial 01 - Introduction To MVC (Java Brains, YouTube)
Test yourself and earn a certificate
10 quick questions. Score 70%+ to download your Struts certificate.
The fundamentals every entry-level round checks. If any answer here surprises you, that's your study list.
Struts is an open-source Java framework for building web applications on the Model-View-Controller pattern, maintained by the Apache Software Foundation. It sits on top of servlets and JSP and gives request handling a clear structure.
The problem it solves is the tangle you get when business logic, HTML, and request routing all live in the same JSP or servlet. Struts pushes routing into a front controller, logic into action classes, and presentation into views, so a growing app stays maintainable.
Key point: A one-line definition plus the problem it solves (separation of concerns in Java web apps) beats a memorized feature list. Interviewers open with this to hear how you frame an answer.
Watch a deeper explanation
Video: java struts tutorial|Java Framework|Struts2.X Part - 1 by Naveen (Durga Software Solutions, YouTube)
MVC splits an app into three parts: the Model holds data and business logic, the View renders output, and the Controller handles input and coordinates the other two. The point is that each part changes independently.
In Struts 2 the controller is the front-controller filter plus the interceptor stack, the model is your action's data and the services it calls, and the view is a JSP or other result type. The action itself sits between them: it takes populated data, runs logic, and returns a result name.
| MVC role | Struts 2 piece | Job |
|---|---|---|
| Controller | Front-controller filter + interceptors | Receive the request, route it, run cross-cutting work |
| Model | Action data + service/DAO layer | Hold state and business logic |
| View | JSP / FreeMarker / result type | Render the response |
Key point: the question needs you to place the action correctly. Saying the action is 'the controller' is a common slip; it's better described as the request handler that sits between the controller layer and the model.
Struts 2 isn't an upgrade of Struts 1; it's a rewrite based on the WebWork project that kept the Struts name. Struts 1 used an ActionServlet, ActionForm beans, and Action classes you had to extend. Struts 2 uses a filter front controller, POJO actions with no required base class, interceptors, and OGNL.
Practically: Struts 1 shared one Action instance across requests (forcing stateless code), while Struts 2 creates a new action per request. Struts 1 kept form data in a separate ActionForm; Struts 2 puts properties on the action itself.
Key point: Naming three concrete differences (instance-per-request, POJO actions, interceptors) is the bar. Vague 'Struts 2 is better' answers get a follow-up.
The front controller is a servlet filter that receives every request, decides which action handles it, runs the interceptor stack, invokes the action, and renders the result. In current Struts 2 it's StrutsPrepareAndExecuteFilter; older versions used FilterDispatcher.
You register it in web.xml (or via the Servlet container's programmatic config) so it maps to the URL pattern your app uses, usually /*.
<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>Key point: Knowing it's a filter in Struts 2 (not the ActionServlet of Struts 1) is the distinction the key signal is.
Watch a deeper explanation
Video: Struts 2 Tutorial 03 - Setting Up (Java Brains, YouTube)
An action is a plain Java object that handles a request. It holds the request data as properties (with getters and setters) and exposes a method, usually execute(), that runs your logic and returns a result name like "success".
It doesn't have to extend anything, though most extend ActionSupport for validation, i18n, and the result-name constants. Because Struts 2 creates a new action per request, you can safely store request state in instance fields.
public class HelloAction extends ActionSupport {
private String name;
public String execute() {
return SUCCESS; // maps to a result in struts.xml
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
}struts.xml is the main configuration file for a Struts 2 app. It defines packages, action mappings (which URL maps to which action class and method), results (what to render for each result name), and interceptor stacks.
It usually lives on the classpath at src/main/resources. Annotations and the convention plugin can replace much of it, but the XML is what most interview examples use.
<struts>
<package name="default" extends="struts-default">
<action name="hello" class="com.example.HelloAction">
<result name="success">/hello.jsp</result>
<result name="input">/form.jsp</result>
</action>
</package>
</struts>Key point: The extends="struts-default" detail matters: it pulls in the default interceptor stack. Leaving it out is a classic 'nothing works' bug.
A request hits the front-controller filter, which consults the configuration to find the matching action mapping. It creates a new action instance, then runs the interceptor stack in order: interceptors handle parameter population, validation, file upload, and more before the action executes.
The action's method runs and returns a result name. Struts maps that name to a result (often a JSP), renders it, and the interceptors run again on the way back out. The value stack carries data throughout so the view can read it via OGNL.
Struts 2 request lifecycle
Interceptors run on the way in and again on the way out, wrapping the action like layers of an onion.
Interceptors are components that run before and after an action to handle cross-cutting concerns: populating request parameters onto the action, running validation, handling file uploads, managing the conversation scope, exception handling, and more. They're configured in a stack that defines execution order.
This is the piece that replaces the hardcoded controller plumbing of Struts 1. Because they're pluggable, you add or remove behavior by editing the stack rather than the action code.
Key point: Naming two or three real interceptors (params, validation, fileUpload) shows you've seen a stack, not just read the word 'interceptor'.
Watch a deeper explanation
Video: Struts 2 Tutorial 16 - Introducing Interceptors (Java Brains, YouTube)
The value stack is a stack of objects Struts keeps for each request, with the current action usually on top. Views and configuration read and write data on it through OGNL, so a JSP tag like <s:property value="name"/> reaches into the action's name property without you wiring it up.
It's part of the ActionContext, the thread-local bag that holds everything about the current request. The stack is why you rarely touch the raw HttpServletRequest in Struts 2.
OGNL, Object-Graph Navigation Language, is the expression language Struts 2 uses to read and write properties on the value stack. When a JSP tag says value="user.email", OGNL walks from the top of the stack to the user object's email property.
It runs form binding, tag output, and dynamic configuration. Because OGNL can call methods and reach into object graphs, careless exposure of user-controlled OGNL is also the root of several Struts security holes, which is why version patching matters.
<%-- reads name off the value stack (the action) --%>
<s:property value="name"/>
<%-- navigates an object graph --%>
<s:property value="user.address.city"/>A result is what Struts renders after an action returns a result name. The mapping from name to result lives in configuration. The default result type is dispatcher, which forwards to a JSP, but there are many others.
Common types: dispatcher (forward to JSP), redirect (browser redirect to a URL), redirectAction (redirect to another action), stream (send bytes, for downloads), json (with the JSON plugin), and freemarker.
| Result type | What it does | Typical use |
|---|---|---|
| dispatcher | Forwards to a JSP (default) | Render a normal page |
| redirect | Sends an HTTP redirect to a URL | Post-redirect-get, external links |
| redirectAction | Redirects to another action | Chaining flows after a form submit |
| stream | Writes raw bytes to the response | File downloads |
| json | Serializes the action to JSON | AJAX endpoints (JSON plugin) |
ActionSupport is a convenience base class that implements several useful interfaces at once: Action (with the SUCCESS, INPUT, ERROR, LOGIN, NONE result constants), Validateable and ValidationAware (for the validate hook and field error collection), and TextProvider and LocaleProvider (for internationalization and message lookup).
You extend it so you get validate(), addFieldError(), getText() for i18n, and the result constants for free. You're never required to; a POJO with an execute() method works, but you'd reimplement what ActionSupport gives you.
The params interceptor (part of the default stack) reads request parameters and sets matching properties on the action via their setters, using OGNL. A form field named email calls setEmail on the action. Nested names like user.email walk into an object.
So there's no separate ActionForm like in Struts 1: the action itself holds the form data. You just add the field, the getter, and the setter.
// <input name="user.email"> populates this automatically
public class SignupAction extends ActionSupport {
private User user = new User();
public User getUser() { return user; }
public void setUser(User user) { this.user = user; }
}Two main ways. Programmatic: override validate() in an action that extends ActionSupport and call addFieldError for each problem; if any errors exist, Struts routes to the input result instead of running the action. Declarative: an XML validation file or annotations describe rules and the validation interceptor applies them.
Both rely on the validation and workflow interceptors in the default stack, and both send failures back to the input result so the user sees their form again with messages.
public void validate() {
if (name == null || name.trim().isEmpty()) {
addFieldError("name", "Name is required");
}
if (age < 18) {
addFieldError("age", "Must be 18 or older");
}
}Key point: Mentioning that validation failures need an <result name="input"> mapping to actually show up is the detail that trips people in practice.
The input result is where the workflow interceptor sends the request when validation fails. Instead of running the action, Struts renders whatever you mapped to the name "input", normally the form page, so the user can fix their input.
If you validate but forget to map an input result, Struts throws an error because it has nowhere to route the failure. That's a frequent beginner bug.
ActionContext is a thread-local container holding everything about the current request: the value stack, request and session parameters, the locale, and the application context. Because it's thread-local, your action reads context without being handed the raw servlet objects.
You reach it with ActionContext.getContext(). It's how Struts keeps actions decoupled from the servlet API while still giving access to session and request data when you need it.
The clean way is to implement SessionAware, which asks Struts to inject a Map view of the session via setSession before the action runs. That keeps the action testable and free of a direct servlet dependency, because you can pass a plain map in a unit test instead of mocking the container.
You can also pull it from ActionContext.getContext().getSession(). Reaching for the raw HttpServletRequest via ServletRequestAware works too but couples the action to the servlet API, which interviewers note as the less clean option.
public class CartAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
public void setSession(Map<String, Object> session) {
this.session = session;
}
public String execute() {
session.put("lastVisit", System.currentTimeMillis());
return SUCCESS;
}
}Struts 2 had several high-severity remote-code-execution vulnerabilities over the years, many tied to OGNL expressions being evaluated on user-controlled input. Unpatched Struts apps have caused large real-world breaches, so which version you run is a security decision, not just a compatibility one, and interviewers ask about it deliberately.
The interview point is practical: run a maintained, patched version, watch Apache security bulletins, and don't expose user input to OGNL evaluation. Showing you know Struts needs active patching, not set-and-forget, indicates production maturity.
Key point: You don't need CVE numbers. Knowing the class of problem (OGNL injection) and the fix (stay patched) is what this screens for.
web.xml is the servlet container's deployment descriptor, and in a Struts 2 app its main job is registering the front-controller filter and mapping it to a URL pattern (usually /*). That single filter registration is what routes every request into Struts.
It can also hold container-level config like listeners (for Spring), welcome files, and error pages. Most of the app's routing lives in struts.xml instead, so web.xml stays small: it hands requests to the filter and gets out of the way.
Key point: The distinction the key signal is is web.xml (container config, registers the filter) versus struts.xml (action routing). Mixing up which file does what is a common slip.
For candidates with working experience: interceptor mechanics, OGNL depth, validation, and the configuration judgment that separates users from understanders.
Interceptors are chained like layers of an onion. Each interceptor gets the ActionInvocation, does its before-work, calls invocation.invoke() to pass control inward, then does its after-work when control returns. So the order in is the reverse of the order out.
The stack is defined in configuration, and the order matters: params must run before validation, and validation before the action. The struts-default stack orders the common interceptors correctly, which is why most packages extend it.
<interceptor-stack name="myStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="params"/>
<interceptor-ref name="validation"/>
<interceptor-ref name="workflow"/>
</interceptor-stack>Key point: The onion model plus invoke() is the answer. If you can explain that before-code runs top-down and after-code runs bottom-up, you understand the mechanism.
Watch a deeper explanation
Video: Struts 2 Tutorial 18 - Anatomy of an Interceptor (Java Brains, YouTube)
Implement the Interceptor interface (or extend AbstractInterceptor to skip the lifecycle methods) and put your logic in intercept(ActionInvocation). Do your before-work, call invocation.invoke() to continue the chain, then do after-work with the returned result.
Register it in struts.xml and add it to a stack. Common uses: audit logging, custom auth checks, request timing. If you skip invoke() and return a result name instead, you short-circuit the chain, which is how a login interceptor blocks unauthenticated requests.
public class AuthInterceptor extends AbstractInterceptor {
public String intercept(ActionInvocation inv) throws Exception {
Map<String, Object> session = inv.getInvocationContext().getSession();
if (session.get("user") == null) {
return Action.LOGIN; // short-circuit: skip the action
}
return inv.invoke(); // continue the chain
}
}Key point: The short-circuit trick (return a result instead of calling invoke) is the follow-up. It's how auth and rate-limiting interceptors work.
OGNL expressions are evaluated against the value stack's top object first, then down the stack, so an unqualified name like email resolves against the action if the action has it. The stack is the root object, so top-level names search each stacked object in order.
The # prefix reaches named context objects instead of the stack: #session, #request, #parameters, and #attr. So value="name" reads the action's name, while value="#session.user" reaches the session map.
<%-- from the value stack (action property) --%>
<s:property value="orderTotal"/>
<%-- from named context objects with # --%>
<s:property value="#session.username"/>
<s:property value="#parameters.page"/>You put a validation file named ActionClass-validation.xml next to the action. It declares field validators (required, stringlength, email, int range, regex) with messages. The validation interceptor reads it and applies the rules before the action runs.
It keeps rules out of Java and lets you reuse validators. Failures collect as field errors and route to the input result, same as programmatic validate().
<validators>
<field name="email">
<field-validator type="requiredstring">
<message>Email is required</message>
</field-validator>
<field-validator type="email">
<message>Enter a valid email</message>
</field-validator>
</field>
</validators>Declarative XML validation fits standard, per-field rules: required, length, format, range. It's reusable, readable, and keeps the action clean, so reach for it first for ordinary form checks where the rule depends on one field and nothing else in the request.
Programmatic validate() fits cross-field and conditional logic: password matches confirmation, end date after start date, a rule that only applies when another field is set. Those are awkward in XML, so put them in Java.
| Concern | Declarative XML | Programmatic validate() |
|---|---|---|
| Single-field rules | Best fit | Overkill |
| Cross-field logic | Awkward | Best fit |
| Reuse across actions | Easy | Manual |
| Readability | Rules in one file | Logic near the action |
Key point: The production-ready answer is 'both': declarative for the routine fields, validate() for the conditional and cross-field checks. Picking one dogmatically is the weaker response.
A namespace maps a package of actions to a URL prefix, so /admin/deleteUser and /user/deleteUser can coexist as different actions. You set namespace on the package element; the default namespace is empty (root).
Namespaces organize large apps and are also a natural boundary for applying different interceptor stacks, for example an auth interceptor on the whole /admin namespace.
<package name="admin" namespace="/admin" extends="struts-default">
<action name="dashboard" class="com.example.admin.DashboardAction">
<result>/admin/dashboard.jsp</result>
</action>
</package>Action chaining uses the chain result type to forward from one action to another within the same request, copying properties from the first action onto the second via the chaining interceptor. It looks convenient for reusing an action's work.
It's usually discouraged: the copied state is implicit and fragile, it breaks the browser's URL/refresh model, and it couples actions tightly. Prefer redirectAction (a clean redirect) or extracting shared logic into a service both actions call.
Key point: Interviewers often ask this to see if you know the anti-pattern. Recommending a shared service or redirectAction over chaining is the answer they want.
You can use * in an action name to match a pattern and reuse the captured part with {1}, {2} in the class or method. So one mapping can route userAdd, userEdit, userDelete to methods on a single action.
It cuts repetitive config, but overusing it hides which URLs exist. A little wildcarding for CRUD variants is fine; wildcarding everything makes the config hard to read.
<action name="user_*" class="com.example.UserAction" method="{1}">
<result name="success">/user/{1}.jsp</result>
</action>
<!-- user_add calls add(), renders /user/add.jsp -->Dynamic Method Invocation (DMI) lets the incoming URL pick which action method runs using the action!method syntax, like user!delete calling the delete method. It saves writing a separate mapping for each method, which looks convenient for a CRUD action with several entry points.
It's disabled by default because it widens the attack surface: letting the URL name arbitrary methods has been part of Struts exploits. If you enable it, restrict it and prefer explicit method attributes or wildcard mappings instead.
Preparable adds a prepare() method that the prepare interceptor calls before parameters are set on the action. You use it to load objects the request will populate, for example fetching the existing entity by id so form fields update it rather than creating a blank one.
It's the hook for the load-then-populate pattern in edit flows, and it runs early enough that the params interceptor can bind form values onto the loaded object.
public class EditUserAction extends ActionSupport implements Preparable {
private Long id;
private User user;
public void prepare() {
user = (id != null) ? userService.find(id) : new User();
}
// params interceptor then sets user.* fields on the loaded object
}ModelDriven lets you push a model object to the top of the value stack so form fields bind directly to model.field instead of action.model.field. You implement getModel() to return the object, and the modelDriven interceptor stacks it.
It cleans up form binding when an action wraps a single domain object. The trade-off: the model sits above the action on the stack, which can surprise people debugging OGNL resolution, so use it deliberately.
public class SaveUserAction extends ActionSupport
implements ModelDriven<User> {
private User user = new User();
public User getModel() { return user; }
// form field name="email" now binds to user.email
}You put message keys in resource bundle properties files (package.properties, package_fr.properties) and read them with getText("key") in actions or <s:text name="key"/> in JSPs. ActionSupport provides getText via TextProvider, and Struts picks the bundle by locale.
The i18n interceptor tracks the user's locale (from a request parameter or session), so switching language is a matter of changing that locale, not the code.
The fileUpload interceptor (in the default stack) parses multipart requests and hands your action three properties per file field: the File itself (name matching the field), plus [field]FileName and [field]ContentType. You add matching getters and setters.
You control limits with struts.multipart.maxSize and the interceptor's allowed types and extensions. The uploaded file is a temp file, so you copy it somewhere permanent in the action.
public class UploadAction extends ActionSupport {
private File upload;
private String uploadFileName;
private String uploadContentType;
// getters/setters omitted
public String execute() throws Exception {
FileUtils.copyFile(upload, new File("/data/" + uploadFileName));
return SUCCESS;
}
}The exception interceptor (first in the default stack) catches exceptions the action throws and maps them to results. You declare exception-mapping entries, globally or per action, that an exception type maps to a result name like "error".
This keeps try/catch out of actions for expected failure paths and gives a consistent error page. The caught exception lands on the value stack as exception and exceptionStack so the error JSP can display details.
<global-exception-mappings>
<exception-mapping exception="java.lang.Exception"
result="error"/>
</global-exception-mappings>
<global-results>
<result name="error">/error.jsp</result>
</global-results>The convention plugin infers configuration from class names and package locations, so an action class named HelloAction in a conventional package maps to /hello with a matching JSP, no struts.xml entry needed. Annotations like @Action, @Result, and @Namespace override the conventions where you need explicit control.
It cuts XML sharply for teams that like convention over configuration. The trade-off is that the routing is implicit, so newcomers have to learn the conventions to find where a URL goes.
Plain servlets force you to parse parameters, manage dispatch, and handle validation and errors by hand in every servlet. Struts factors all of that into the framework: automatic parameter binding, a declarative action-to-result mapping, an interceptor stack for cross-cutting work, and a tag library that binds views to data.
The cost is a framework to learn and configuration to maintain. For a two-page app that's overhead; for a form-heavy business app it removes a lot of repetitive plumbing.
| Concern | Plain servlets + JSP | Struts 2 |
|---|---|---|
| Parameter binding | Manual request.getParameter | Automatic via params interceptor |
| Request routing | web.xml + hand-written dispatch | Declarative action mappings |
| Cross-cutting logic | Repeated in each servlet | Interceptor stack |
| Validation | Hand-written | Declarative or validate() |
| View data binding | Scriptlets / EL by hand | Struts tags + OGNL |
Both are MVC web frameworks with a front controller and annotated or configured handlers. Spring MVC integrates natively with the wider Spring ecosystem (dependency injection, data, security, Boot) and is the common default for new Java web work today. Struts 2 is a capable MVC framework but mostly lives in existing apps now.
The honest interview answer: for greenfield work most teams reach for Spring MVC or Spring Boot; Struts skills are valuable for maintaining and migrating the large base of existing Struts applications. Saying that indicates current rather than defensive.
Key point: Interviewers testing seniority want to hear you place Struts accurately, a maintenance and migration skill, rather than oversell it against Spring.
The struts2-spring-plugin lets Spring manage your action instances and inject dependencies (services, DAOs) into them. You register the Spring ContextLoaderListener, add the plugin, and set the ObjectFactory to Spring so Struts asks Spring to create actions.
After that, actions become Spring beans: you autowire a service into an action instead of newing it up, which makes actions testable and keeps wiring in one place.
<!-- web.xml -->
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<!-- with struts2-spring-plugin on the classpath, actions are Spring-managed -->Yes, by default, because Struts 2 creates a new action instance for every request. Two concurrent requests get two separate objects, so instance fields holding request data don't collide. This is a direct improvement over Struts 1, which shared one Action instance and forced stateless code.
The caveat: shared dependencies you inject (a service, a cache, a static field) are still shared across requests, so those must be thread-safe themselves. The per-request instance protects request state, not everything the action touches.
Key point: The contrast with Struts 1's single shared Action instance is the point of the question. Note the injected-dependency caveat and you've given the full answer.
Add the struts2-json-plugin, extend the json-default package, and use the json result type. The plugin serializes the action's properties (or a named root object) to JSON, so your action just sets fields and returns success.
You control which fields serialize with includeProperties or excludeProperties parameters on the result. It's how Struts apps expose AJAX endpoints without hand-writing serialization.
<package name="api" extends="json-default">
<action name="user" class="com.example.UserApiAction">
<result type="json">
<param name="root">user</param>
</result>
</action>
</package>Setting struts.devMode to true turns on development conveniences: configuration reloads without a container restart, more detailed error pages, and OGNL and localization warnings surfaced instead of swallowed. It speeds up the edit-refresh loop when you're wiring actions and results and something isn't binding.
It's strictly for development. In production it hurts performance and leaks internal detail in errors, so you turn it off. Interviewers sometimes ask this to check you know it's a dev-only switch.
A package can extend another with the extends attribute, inheriting its interceptor stacks, result types, and global results. Almost every package extends struts-default (from struts-default.xml), which is where the default interceptor stack and standard result types come from. Miss that extends and your actions get no parameter binding or validation.
You use inheritance to define a base package once (a shared auth stack, common global results, a custom result type) and have feature packages extend it. That keeps configuration DRY across modules and makes a cross-cutting change one edit instead of many.
<package name="base" extends="struts-default" abstract="true">
<interceptor-stack name="secured">
<interceptor-ref name="auth"/>
<interceptor-ref name="defaultStack"/>
</interceptor-stack>
<global-results><result name="login">/login.jsp</result></global-results>
</package>
<package name="admin" namespace="/admin" extends="base">
<!-- inherits the secured stack and login result -->
</package>Key point: The 'forgot extends=struts-default so nothing binds' bug is a favorite follow-up. Naming it shows you've debugged real config, not just read the docs.
advanced rounds probe internals, security history, migration judgment, and production scars. Expect every answer here to draw a follow-up.
Several severe Struts 2 remote-code-execution flaws came from user-controlled input being evaluated as OGNL. Because OGNL can call methods and reach the runtime, an attacker who gets an OGNL expression evaluated can run arbitrary code on the server. Some flaws were in how the framework itself parsed certain inputs (error messages, Content-Type headers, tag attributes).
Defenses: stay on a patched Struts version and watch Apache security bulletins, never pass user input into OGNL evaluation or dynamic tag attributes, avoid features like Dynamic Method Invocation unless restricted, and keep the OGNL and framework config locked down. This is the security topic senior interviews expect you to speak to concretely.
Key point: You don't need CVE numbers, but naming the pattern (untrusted input reaching OGNL) and the layered defenses (patch, don't evaluate user input, restrict DMI) is what separates The production-ready answer here.
Watch a deeper explanation
Video: Simple Apache Struts 2.5 Example (Cameron McKenzie, YouTube)
It's a rewrite of the web tier, not a config swap, because the models differ. Plan it incrementally: run both frameworks side by side (Struts 1 and Struts 2 can coexist in one web app with different URL mappings) and migrate feature by feature rather than big-bang.
Per feature: convert Action classes to POJO actions, fold ActionForm data into action properties, replace struts-config.xml mappings with struts.xml or annotations, swap the Struts 1 tags for Struts 2 tags with OGNL, and move validation to validate() or XML validators. Keep the service and DAO layer as-is; only the web tier changes.
Struts 1 to Struts 2 migration approach
Keep the service and persistence layers untouched. Only the web tier is framework-specific, which is what makes incremental migration workable.
Key point: The production signal is 'incremental, side by side, feature by feature'. Proposing a big-bang rewrite of a large app is the answer that worries interviewers.
ActionInvocation holds the ordered list of interceptors, the action, and the result. Each interceptor's intercept() calls invocation.invoke(), which pops the next interceptor and calls it; when the list is exhausted, invoke() runs the action and captures the result name.
That recursion is the onion: every invoke() call nests one level deeper, and the after-code in each interceptor runs as the recursion unwinds. Returning a result name from an interceptor instead of calling invoke() short-circuits the whole chain, which is how auth interceptors block requests.
Implement the Result interface (or extend StrutsResultSupport) and put your rendering logic in execute(ActionInvocation), then register the result type in a package. You get access to the action and value stack, so you can render anything: a CSV export, a PDF, a signed redirect, a templated email preview.
It's worth it when a rendering concern repeats across actions and doesn't fit dispatcher, stream, or json. Bundling it as a result type keeps the actions clean and the behavior reusable.
public class CsvResult extends StrutsResultSupport {
protected void doExecute(String location, ActionInvocation inv)
throws Exception {
HttpServletResponse resp = ServletActionContext.getResponse();
resp.setContentType("text/csv");
Object data = inv.getStack().findValue(location);
writeCsv(resp.getWriter(), data);
}
}The value stack is an OgnlValueStack wrapping a CompoundRoot, a list of objects searched top to bottom. Resolving an unqualified name walks each object looking for a matching property, so a deep stack means more objects to search per expression.
In practice OGNL reflection and stack searching are cheap next to I/O, but two things bite at scale: expressions evaluated in tight loops in a JSP, and very deep stacks from heavy action chaining or nested ModelDriven. Keeping the stack shallow and hoisting repeated OGNL lookups out of loops is the tuning that matters.
Because actions are POJOs, the core logic tests like any other object: construct the action, set its properties, call execute(), assert the returned result name and the resulting state. No container needed for that path, which is a real advantage over Struts 1.
For anything touching the framework (interceptors, the value stack, validation wiring), the struts2-junit-plugin provides StrutsTestCase, which spins up the Struts context so you can drive a full action through its interceptors and check results end to end.
@Test
void validInputSucceeds() {
HelloAction action = new HelloAction();
action.setName("Asha");
assertEquals(Action.SUCCESS, action.execute());
assertEquals("Asha", action.getName());
}Key point: The point to make is testability by design: POJO actions test without a container, which is a headline Struts 2 improvement over Struts 1's servlet-bound Actions.
Measure before you change anything: most Struts app latency is in the database and downstream calls, not the framework. Profile a realistic load and fix the biggest cost first, which is usually N+1 queries, missing caching, or slow service calls, not OGNL.
Framework-level levers once you've ruled out the obvious: turn off devMode in production, trim the interceptor stack to what each package needs, keep the value stack shallow, cache expensive lookups, and precompile JSPs. Enable OGNL and config caching that the framework already provides rather than fighting the framework.
Split struts.xml into per-module files with the include element, so each team or feature owns its own config file and the root just includes them. Combine that with namespaces so URL prefixes match module boundaries, and package inheritance so modules extend a shared base stack.
The convention plugin can remove much of the XML entirely by inferring mappings. The judgment call is consistency: pick XML, annotations, or convention as the team standard rather than mixing all three, because a half-XML half-annotated codebase is hard to reason about.
<struts>
<include file="struts-admin.xml"/>
<include file="struts-billing.xml"/>
<include file="struts-api.xml"/>
</struts>Struts converts string request parameters to action property types automatically: OGNL handles primitives, dates, and collections, converting form strings to ints, booleans, Dates, and List or Map properties. Conversion errors are collected and can route to the input result.
For custom types you write a TypeConverter (extending StrutsTypeConverter) and register it globally in xwork-conversion.properties or per property in a ClassName-conversion.properties file. That's how you bind, say, a comma-separated string to a domain object or a custom date format.
public class MoneyConverter extends StrutsTypeConverter {
public Object convertFromString(Map ctx, String[] values, Class toType) {
return Money.parse(values[0]); // "12.50" -> Money
}
public String convertToString(Map ctx, Object o) {
return ((Money) o).toDisplay();
}
}A local result is declared inside an action and only that action can return it. A global result, declared once per package in global-results, is available to every action in the package, so you don't repeat common outcomes like error or login in every mapping.
Use locals for outcomes specific to one action (a particular success page), and globals for shared outcomes: a common error page, a login redirect, a not-found result. Globals pair naturally with global exception mappings.
The conversionError interceptor turns failed conversions (a non-numeric age) into field errors, and the validation and workflow interceptors turn failed rules into field errors too. Both funnel to the input result, so the user sees one form with all their mistakes flagged.
The clean pattern is to let conversion errors and validation errors coexist: order the interceptors so conversionError runs, then validation, then workflow routes to input if either produced errors. Fighting this by catching conversion in the action is the messier path.
Struts 2 is a servlet-based framework, so it runs anywhere a servlet container does: Tomcat or Jetty in a Docker image, behind a load balancer, scaled horizontally like any stateless web app. Keep session state out of the app (externalize to Redis or a sticky-session-free design) and it scales fine.
For long-running or async work, don't block the request thread inside an action; hand the job to an executor or a queue and return quickly, or use the servlet container's async support at the filter layer. Struts itself is synchronous request-response, so heavy async patterns live around it, not inside the action.
XWork is the command-and-interceptor framework Struts 2 was originally built on, inherited from WebWork. It provided the action invocation model, the interceptor chain, the value stack, OGNL integration, and validation, all independent of the web. Struts 2 layered the servlet-specific pieces (the filter, result types, tags) on top.
Over time XWork was absorbed into the Struts codebase, so you see it in package names and older docs. Knowing this explains why so much of Struts 2's core is web-agnostic and why actions test without a container.
Struts UI tags render through themes: the default xhtml theme wraps each field in a table row with a label and error slot, the simple theme renders just the raw input, and css_xhtml uses CSS instead of tables. You set the theme per tag, per form, or globally.
To customize, you copy the FreeMarker templates for a theme, edit the markup, and point Struts at your template directory, or write a new theme extending an existing one. Most teams switch to simple and control markup themselves rather than fight the table-based defaults.
ActionContext is the framework-neutral, thread-local map holding the value stack, parameters, session, and locale for the current request; it's the abstraction actions should use. ServletActionContext extends it with servlet-specific access to the raw HttpServletRequest, HttpServletResponse, and ServletContext when you genuinely need them.
The value stack lives inside ActionContext and is the OGNL root. So the layering is: ActionContext is the bag, the value stack is one item in it, and ServletActionContext is the escape hatch to the servlet API. Preferring ActionContext keeps actions decoupled and testable.
For most new Java web work, because the ecosystem has moved to Spring Boot and, for APIs, lighter stacks. Struts 2's strengths (structured MVC, interceptors, tag-driven forms) are well served by Spring MVC with a larger, more actively developed ecosystem and better integration with data, security, and cloud tooling.
Struts still makes sense when you're extending an existing Struts application, when a team's whole operational knowledge is Struts, or during a migration where consistency beats rewriting. The mature coverage names Struts as a maintenance and migration technology and reserves new-build recommendations for Spring or similar.
Key point: the question needs honesty and current context, not loyalty to whatever framework the role uses. Placing Struts accurately is the strongest answer.
Struts builds its configuration from several providers in order: default framework settings, struts-default.xml (the base package and interceptors), plugin configs, struts.xml and its includes, then annotation and convention-based config from the convention plugin. Later providers can override earlier ones, and struts.properties or struts.xml constants tune framework settings.
The practical rule when something surprises you: explicit struts.xml usually wins over convention defaults, and package inheritance means an action's effective interceptor stack is whatever its package resolves to. Tracing that resolution order is how you debug a mapping that isn't behaving.
Struts actions are per-request, so a multi-step flow needs somewhere to hold partial state between steps. The straightforward option is the session: store the in-progress object under a key and load it at the start of each step, clearing it when the wizard finishes or is abandoned.
For cleaner scoping, the scopedModelDriven interceptor (or a token-based approach) ties a model to a conversation so parallel wizards in different tabs don't stomp each other. Pair that with the token interceptor to block double submits. The trade-off to name is session memory growth, so you evict abandoned conversations.
public class WizardAction extends ActionSupport implements SessionAware {
private Map<String, Object> session;
public void setSession(Map<String, Object> s) { this.session = s; }
private Order draft;
public void prepare() {
draft = (Order) session.getOrDefault("wizard.order", new Order());
}
public String step2() {
session.put("wizard.order", draft); // persist between steps
return SUCCESS;
}
}Key point: the question needs the state-location decision (session vs conversation scope) plus the double-submit guard (token interceptor) plus the cleanup concern. Naming all three indicates production experience.
Struts 2 is not an upgrade of Struts 1; it's a different framework that kept the name. Struts 1 tied you to a servlet-aware Action base class and ActionForm beans. Struts 2 actions are plain objects with no framework base class required, tested without a container, and wired through interceptors instead of hardcoded controller logic. Spring MVC is the framework most teams pick for new work today, so knowing where each fits, and being honest that Struts is mostly a maintenance and migration skill now, indicates current rather than dated.
| Aspect | Struts 1 | Struts 2 | Spring MVC |
|---|---|---|---|
| Action model | Extend Action, one instance shared | POJO action, new instance per request | Annotated @Controller methods |
| Form data | Separate ActionForm bean | Properties on the action itself | Command objects / @ModelAttribute |
| Controller wiring | struts-config.xml | Interceptor stack + struts.xml/annotations | DispatcherServlet + annotations |
| Expression language | JSTL / EL | OGNL + value stack | SpEL / EL |
| Status | End of life (2013) | Maintained, mostly legacy apps | Actively developed, common default |
Prepare in layers, and practice out loud. Most Struts rounds move from concept questions about MVC and the request lifecycle to reading or writing action and configuration code, then a discussion of interceptors, validation, or a migration. Rehearse each stage rather than only reading answers.
The typical Struts interview flow
Earlier rounds increasingly run as AI coding interviews. The concepts on this page are what gets probed at every stage.
10 questions, about 6 minutes. Score 70% or higher to earn a shareable certificate.
Hyring builds the AI Coding Interviewer that runs live technical rounds in 20+ languages for 5,000+ hiring teams. These Struts questions and scoring notes reflect what actually gets asked and evaluated inside the interviews we host.
See how the AI Coding Interviewer works