diff --git a/frontend/Makefile b/frontend/Makefile
index 56386b6dbb0d0b1e6c7a17054c37485ccee4baa4..45d9a5e9cc355255402525de44bdb1153507512d 100644
--- a/frontend/Makefile
+++ b/frontend/Makefile
@@ -6,7 +6,7 @@ all: test_client frontend_ssi.so
 
 # XrdSsi server plugin
 
-frontend_ssi.so: TestSsiServiceProvider.o TestSsiService.o TestSsiRequestProc.o test.pb.o
+frontend_ssi.so: TestSsiServiceProvider.o TestSsiRequestProc.o test.pb.o
 	$(LINK.cc) -shared $^ $(LOADLIBES) $(LDLIBS) -o $@
 
 # XrdSsi test client
@@ -35,4 +35,4 @@ clean:
 	rm -f   frontend_ssi.o frontend_ssi.so \
 		test_client test_client.o TestSsiRequest.o \
 		test.pb.cc test.pb.h test.pb.o \
-		TestSsiRequestProc.o TestSsiService.o TestSsiServiceProvider.o
+		TestSsiRequestProc.o TestSsiServiceProvider.o
diff --git a/frontend/TestSsiClient.h b/frontend/TestSsiClient.h
index f82c1b968829459070064b7d9dfcbc5b76bc9a94..b5a228f51e6f628dca97e378cfbd6b91ae166702 100644
--- a/frontend/TestSsiClient.h
+++ b/frontend/TestSsiClient.h
@@ -2,10 +2,10 @@
 #define __TEST_SSI_CLIENT_H
 
 #include <iostream>
-#include <stdexcept>
 
 #include <XrdSsi/XrdSsiProvider.hh>
 #include <XrdSsi/XrdSsiService.hh>
+#include "XrdSsiException.h"
 #include "TestSsiRequest.h"
 
 // Probably we want to allow multiple resources, e.g. streaming and non-streaming versions of the service
@@ -15,22 +15,6 @@ const std::string TestSsiResource("/test");
 
 
 
-// Class to convert a XRootD error into a std::exception
-
-class XrdSsiException : public std::exception
-{
-public:
-   XrdSsiException(const std::string &err_msg) : error_msg(err_msg)     {}
-   XrdSsiException(const XrdSsiErrInfo &eInfo) : error_msg(eInfo.Get()) {}
-
-   const char* what() const noexcept { return error_msg.c_str(); }
-
-private:
-   std::string error_msg;
-};
-
-
-
 // XrdSsiProviderClient is instantiated and managed by the SSI library
 
 extern XrdSsiProvider *XrdSsiProviderClient;
@@ -84,17 +68,17 @@ public:
 
       // Serialize the request object
 
-      std::string data;
+      std::string request_str;
 
-      if(!request_msg.SerializeToString(&data))
+      if(!request_msg.SerializeToString(&request_str))
       {
          throw XrdSsiException("SerializeToString() failed");
       }
 
-      requestP = new TestSsiRequest(data, timeout);
+      requestP = new TestSsiRequest(request_str, timeout);
 
       // Transfer ownership of the request to the service object
-      // TestSsiRequest handles deletion of the data buffer, so we can allow the pointer to go out-of-scope
+      // TestSsiRequest handles deletion of the request buffer, so we can allow the pointer to go out-of-scope
 
       serverP->ProcessRequest(*requestP, resource);
 
diff --git a/frontend/TestSsiRequestProc.cpp b/frontend/TestSsiRequestProc.cpp
index 44b629cef24df55e69bb1c6c4d20158a976d141c..6e761eefa38c4528ae1f0a5b95e963545203f2bf 100644
--- a/frontend/TestSsiRequestProc.cpp
+++ b/frontend/TestSsiRequestProc.cpp
@@ -1,46 +1,16 @@
 #include <iostream>
 
+#include "XrdSsiException.h"
 #include "TestSsiRequestProc.h"
+#include "TestSsiProtobuf.h"
 
-void RequestProc::Execute()
-{
-   using namespace std;
-
-   const string metadata("Have some metadata!");
-   const string response("Have a response!");
-
-   cerr << "Execute()" << endl;
-
-   // Deserialize the Request
-
-   //int reqLen;
-   //const char *reqData = GetRequest(reqLen);
-
-   // Parse the request
-
-   ReleaseRequestBuffer(); // Optional
-
-   // Perform the requested action
-
-   // Optional: send alerts
-
-   // Optional: send metadata ahead of the response
+// This is for specialized private methods called by RequestProc::Execute to handle actions, alerts
+// and metadata
 
-   SetMetadata(metadata.c_str(), metadata.size());
-
-   // Send the response
-
-   SetResponse(response.c_str(), response.size());
-}
-
-
-
-void RequestProc::Finished(XrdSsiRequest &rqstR, const XrdSsiRespInfo &rInfo, bool cancel)
+template <>
+void RequestProc<xrdssi::test::Request, xrdssi::test::Result>::ExecuteMetadata()
 {
-   using namespace std;
-
-   cerr << "Finished()" << endl;
-
-   // Reclaim any allocated resources
+   const std::string metadata("Have some metadata!");
+   SetMetadata(metadata.c_str(), metadata.size());
 }
 
diff --git a/frontend/TestSsiRequestProc.h b/frontend/TestSsiRequestProc.h
index 4499b95827a99114c2450f366db17cd3412802ee..82e3e2aef97d79702388468c0d1cb0d05ea13323 100644
--- a/frontend/TestSsiRequestProc.h
+++ b/frontend/TestSsiRequestProc.h
@@ -2,6 +2,7 @@
 #define __TEST_SSI_REQUEST_PROC_H
 
 #include <XrdSsi/XrdSsiResponder.hh>
+#include "XrdSsiException.h"
 
 /*
  * The XrdSsiResponder class knows how to safely interact with the request object. It allows handling asynchronous
@@ -17,14 +18,92 @@
  * RequestProc is a kind of agent object that the service object creates for each request that it receives.
  */
 
+template <typename RequestType, typename ResponseType>
 class RequestProc : public XrdSsiResponder
 {
 public:
-            RequestProc() {}
-   virtual ~RequestProc() {}
+                RequestProc() {}
+   virtual     ~RequestProc() {}
 
            void Execute();
    virtual void Finished(XrdSsiRequest &rqstR, const XrdSsiRespInfo &rInfo, bool cancel=false) override;
+
+private:
+   void ExecuteAction()   {}
+   void ExecuteAlerts()   {}
+   void ExecuteMetadata() {}
+
+   RequestType  request;
+   ResponseType response;
 };
 
+
+
+template <typename RequestType, typename ResponseType>
+void RequestProc<RequestType, ResponseType>::Execute()
+{
+   using namespace std;
+
+   cerr << "Execute()" << endl;
+
+   // Unpack the Request buffer into a string object.
+   //
+   // We need to construct this with an explicit length, as request_buffer is a binary buffer, not a
+   // null-terminated string.
+
+   int request_len;
+   const char *request_buffer = GetRequest(request_len);
+   const std::string request_str(request_buffer, request_len);
+
+   // Deserialize the Request
+
+   if(!request.ParseFromString(request_str))
+   {
+      throw XrdSsiException("ParseFromString() failed");
+   }
+
+   // Release the request buffer (optional, perhaps it is more efficient to reuse it)
+
+   ReleaseRequestBuffer();
+
+   // Perform the requested action
+
+   ExecuteAction();
+
+   // Optional: send alerts
+
+   ExecuteAlerts();
+
+   // Optional: send metadata ahead of the response
+
+   ExecuteMetadata();
+
+   // Serialize the Response
+
+   std::string response_str;
+
+   if(!response.SerializeToString(&response_str))
+   {
+      throw XrdSsiException("SerializeToString() failed");
+   }
+
+   // Send the response
+
+   SetResponse(response_str.c_str(), response_str.length());
+}
+
+
+
+// Create specialized versions of this method to handle cancellation/cleanup for specific message types
+
+template <typename RequestType, typename ResponseType>
+void RequestProc<RequestType, ResponseType>::Finished(XrdSsiRequest &rqstR, const XrdSsiRespInfo &rInfo, bool cancel)
+{
+   using namespace std;
+
+   cerr << "Finished()" << endl;
+
+   // Reclaim any allocated resources
+}
+
 #endif
diff --git a/frontend/TestSsiService.cpp b/frontend/TestSsiService.cpp
deleted file mode 100644
index 90d0dfc10b2560065ced27e409fb519ac20b6cb4..0000000000000000000000000000000000000000
--- a/frontend/TestSsiService.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-#include <iostream>
-
-#include "TestSsiService.h"
-#include "TestSsiRequestProc.h"
-
-
-
-void TestSsiService::ProcessRequest(XrdSsiRequest &reqRef, XrdSsiResource &resRef)
-{
-   using namespace std;
-
-   cerr << "Called ProcessRequest()" << endl;
-
-   RequestProc theProcessor;
-
-   // Bind the processor to the request. This works because the
-   // it inherited the BindRequest method from XrdSsiResponder.
-
-   theProcessor.BindRequest(reqRef);
-
-   // Execute the request, upon return the processor is deleted
-
-   theProcessor.Execute();
-
-   // Unbind the request from the responder (required)
-
-   theProcessor.UnBindRequest();
-
-   cerr << "ProcessRequest.UnBind()" << endl;
-}
-
diff --git a/frontend/TestSsiService.h b/frontend/TestSsiService.h
index f679869a4c9290f66bcffb9320c4a9f13673eb28..168faad9992bb57ebd3bec4023ee107c9a8b4383 100644
--- a/frontend/TestSsiService.h
+++ b/frontend/TestSsiService.h
@@ -3,30 +3,55 @@
 
 #include <XrdSsi/XrdSsiService.hh>
 
+#include "TestSsiRequestProc.h"
+
 
 
 /*
  * Service Object, obtained using GetService() method of the TestSsiServiceProvider factory
  */
 
+template <typename RequestType, typename ResponseType>
 class TestSsiService : public XrdSsiService
 {
 public:
+            TestSsiService() {}
+   virtual ~TestSsiService() {}
+
+   // The pure abstract method ProcessRequest() is called when the client calls its ProcessRequest() method to hand off
+   // its request and resource objects. The client’s request and resource objects are transmitted to the server and passed
+   // into the service’s ProcessRequest() method.
 
-// The pure abstract method ProcessRequest() is called when the client calls its ProcessRequest() method to hand off
-// its request and resource objects. The client’s request and resource objects are transmitted to the server and passed
-// into the service’s ProcessRequest() method.
+   virtual void ProcessRequest(XrdSsiRequest &reqRef, XrdSsiResource &resRef) override;
 
-virtual void ProcessRequest(XrdSsiRequest &reqRef, XrdSsiResource &resRef) override;
+   // Additional virtual methods:
+   //
+   // Attach(): optimize handling of detached requests
+   // Prepare(): perform preauthorization and resource optimization
+};
 
-// Additional virtual methods:
-//
-// Attach(): optimize handling of detached requests
-// Prepare(): perform preauthorization and resource optimization
 
-         TestSsiService() {}
-virtual ~TestSsiService() {}
 
-};
+template <typename RequestType, typename ResponseType>
+void TestSsiService<RequestType, ResponseType>::ProcessRequest(XrdSsiRequest &reqRef, XrdSsiResource &resRef)
+{
+   std::cerr << "Called ProcessRequest()" << std::endl;
+
+   RequestProc<RequestType, ResponseType> processor;
+
+   // Bind the processor to the request. Inherits the BindRequest method from XrdSsiResponder.
+
+   processor.BindRequest(reqRef);
+
+   // Execute the request, upon return the processor is deleted
+
+   processor.Execute();
+
+   // Unbind the request from the responder (required)
+
+   processor.UnBindRequest();
+
+   std::cerr << "ProcessRequest.UnBind()" << std::endl;
+}
 
 #endif
diff --git a/frontend/TestSsiServiceProvider.cpp b/frontend/TestSsiServiceProvider.cpp
index 9bfac64216f10d6bd7bc8256b93523c57925fb5f..6e2c24607f1c2dfdce3700454b7d65a1e36130e4 100644
--- a/frontend/TestSsiServiceProvider.cpp
+++ b/frontend/TestSsiServiceProvider.cpp
@@ -43,7 +43,7 @@ XrdSsiService* TestSsiServiceProvider::GetService(XrdSsiErrInfo &eInfo, const st
 
    cerr << "Called GetService(" << contact << "," << oHold << ")" << endl;
 
-   XrdSsiService *ptr = new TestSsiService;
+   XrdSsiService *ptr = new TestSsiService<xrdssi::test::Request, xrdssi::test::Result>;
 
    return ptr;
 }
diff --git a/frontend/XrdSsiException.h b/frontend/XrdSsiException.h
new file mode 100644
index 0000000000000000000000000000000000000000..0dd36f15d58e8afc23c93dcac24bbe0c22b93b38
--- /dev/null
+++ b/frontend/XrdSsiException.h
@@ -0,0 +1,24 @@
+#ifndef __XRD_SSI_EXCEPTION_H
+#define __XRD_SSI_EXCEPTION_H
+
+// Class to convert a XRootD error into a std::exception
+// Perhaps should be part of XRootD?
+
+#include <stdexcept>
+#include <XrdSsi/XrdSsiErrInfo.hh>
+
+
+
+class XrdSsiException : public std::exception
+{
+public:
+   XrdSsiException(const std::string &err_msg) : error_msg(err_msg)     {}
+   XrdSsiException(const XrdSsiErrInfo &eInfo) : error_msg(eInfo.Get()) {}
+
+   const char* what() const noexcept { return error_msg.c_str(); }
+
+private:
+   std::string error_msg;
+};
+
+#endif