Big controllers feel clever until the third bug report in the same week.
\n\n\n\nSmall servlets are quiet and boring and they keep your weekend free.
\n\n\n\nWe have Rails noise on every feed and still a lot of us ship on Java Servlets every day. Tomcat 6 runs steady, Spring and Struts are everywhere, and plenty of in house apps still ride plain HttpServlet with a splash of JSTL. The trick that saves teams is not a new framework but a simple habit. Keep controllers tiny and focused. One servlet does one thing and speaks one URL with a clear name. doGet and doPost should read like a short checklist and then hand off to a service that does the heavy lifting. When a controller gets fat, bugs love it and performance gets weird at the edges.
\n\n\n\nWrite a servlet that only parses input, calls a service, and chooses a view. That is it. All validation sits close to the parse step with clear messages so you do not chase stack traces across layers. Use strong parameter names, default values that make sense, and early returns when input is bad. Move business rules to a plain class and keep it free from servlet APIs so you can test it with JUnit in a snap. Keep the request scope clean and explicit so your JSP does not guess. This way your controller stays under fifty lines and you can read it without scrolling.
\n\n\n\npublic class CreateOrderServlet extends HttpServlet {\n private final OrderService service = new OrderService();\n\n @Override\n protected void doPost(HttpServletRequest req, HttpServletResponse resp)\n throws IOException, ServletException {\n\n String customerId = req.getParameter("customerId");\n String sku = req.getParameter("sku");\n int qty = parseInt(req.getParameter("qty"), 1);\n\n if (isBlank(customerId) || isBlank(sku) || qty < 1) {\n req.setAttribute("error", "Please send customerId, sku, and a positive qty");\n forward(req, resp, "/WEB-INF/views/orderForm.jsp");\n return;\n }\n\n try {\n Order order = service.create(customerId, sku, qty);\n req.setAttribute("order", order);\n forward(req, resp, "/WEB-INF/views/orderOk.jsp");\n } catch (OutOfStock e) {\n req.setAttribute("error", e.getMessage());\n forward(req, resp, "/WEB-INF/views/orderForm.jsp");\n }\n }\n\n private void forward(HttpServletRequest req, HttpServletResponse resp, String view)\n throws ServletException, IOException {\n req.getRequestDispatcher(view).forward(req, resp);\n }\n\n private boolean isBlank(String s) { return s == null || s.trim().isEmpty(); }\n private int parseInt(String n, int def) { try { return Integer.parseInt(n); } catch(Exception e){ return def; } }\n}\n\nclass OrderService {\n Order create(String customerId, String sku, int qty) {\n // domain work here, no servlet imports\n return new Order(customerId, sku, qty);\n }\n}\n\n\n\nSmall servlets shine when things change. A new rule lands from sales and you touch the service not the controller. A new field shows up and you only tweak the top of doPost or doGet and the view. You can even plug the same service into Spring or a Quartz job later without ripping wires. Keep URLs human and stable and your team starts to guess the mapping before opening code. Name your servlet class to match the action and the JSP to match the outcome. These boring names do more for a team than any clever abstraction.
\n\n\n\n<!-- web.xml mapping that reads like a sentence -->\n<servlet>\n <servlet-name>createOrder</servlet-name>\n <servlet-class>com.acme.web.CreateOrderServlet</servlet-class>\n</servlet>\n<servlet-mapping>\n <servlet-name>createOrder</servlet-name>\n <url-pattern>/orders/create</url-pattern>\n</servlet-mapping>\n\n<!-- keep cross cutting in filters, not in controllers -->\n<filter>\n <filter-name>charset</filter-name>\n <filter-class>com.acme.web.EncodingFilter</filter-class>\n</filter>\n<filter-mapping>\n <filter-name>charset</filter-name>\n <url-pattern>/*</url-pattern>\n</filter-mapping>\n\n\n\nFilters carry cross cutting stuff so controllers stay tidy. Put character encoding in a single filter, same for auth checks and simple logging. Keep JSPs clean with JSTL and avoid secret work in scriptlets, since hidden logic always comes back to bite. For test flow, write thin tests that run the service by itself and a few Servlet API fakes to check wiring and view names. You can stub request params with a tiny helper and assert that the right JSP loaded and the right request attributes exist. A fast feedback loop beats any fancy tool right now and will probably still beat it next quarter.
\n\n\n\n// a tiny test around the service\npublic class OrderServiceTest {\n @org.junit.Test\n public void createsOrder() {\n OrderService s = new OrderService();\n Order o = s.create("c123", "ABC", 2);\n org.junit.Assert.assertEquals(2, o.getQty());\n }\n}\n\n// a super small fake request for controller checks\nclass FakeRequest extends javax.servlet.http.HttpServletRequestWrapper {\n private final java.util.Map<String,String> params = new java.util.HashMap<>();\n private final java.util.Map<String,Object> attrs = new java.util.HashMap<>();\n public FakeRequest() { super(new org.apache.catalina.connector.RequestFacade(new org.apache.catalina.connector.Request())); }\n public void setParam(String k, String v) { params.put(k, v); }\n @Override public String getParameter(String k) { return params.get(k); }\n @Override public void setAttribute(String k, Object v) { attrs.put(k, v); }\n @Override public Object getAttribute(String k) { return attrs.get(k); }\n}\n\n\n\nSmall controllers age well and leave you more time to sip coffee than to chase ghosts.
\n