diff --git a/java/3-4-Undo/Main.java b/java/3-4-Undo/Main.java
new file mode 100644
index 0000000000000000000000000000000000000000..57e0370161dd225e77f002174cbca83283ee53b6
--- /dev/null
+++ b/java/3-4-Undo/Main.java
@@ -0,0 +1,45 @@
+// CS 349 Undo Demo
+
+import javax.swing.*;
+import javax.swing.event.UndoableEditEvent;
+import javax.swing.undo.UndoManager;
+
+import java.awt.Dimension;
+import java.awt.GridLayout;
+import java.awt.event.*;
+
+public class Main {
+	
+	Model model;
+
+	public static void main(String[] args) {
+		new Main();
+	}
+
+	public Main() {
+
+		JFrame frame = new JFrame("UndoDemo");
+
+		// create Model and initialize it
+		// (value, min, max in this model)
+		model = new Model(22, 0, 100);
+
+		// create View
+		View view = new View(model);
+		
+		// create Menu View
+		MainMenuView menuView = new MainMenuView(model);
+
+		// add views to the window
+		frame.getContentPane().add(view);
+		frame.setJMenuBar(menuView);
+
+		frame.setPreferredSize(new Dimension(500, 120));
+		frame.pack();
+		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+		frame.setVisible(true);
+
+		// let all the views know that they're connected to the model
+		model.updateViews();
+	}
+}
diff --git a/java/3-4-Undo/MainMenuView.java b/java/3-4-Undo/MainMenuView.java
new file mode 100644
index 0000000000000000000000000000000000000000..a856be44f57eedd0e94f4e98c2f59610d6ee0ae2
--- /dev/null
+++ b/java/3-4-Undo/MainMenuView.java
@@ -0,0 +1,78 @@
+// CS 349 Undo Demo
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.KeyEvent;
+import java.util.Observable;
+import java.util.Observer;
+
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.KeyStroke;
+
+
+// a main menu view
+public class MainMenuView extends JMenuBar implements Observer {
+	
+	// Undo menu items
+	private JMenuItem undoMenuItem;
+	private JMenuItem redoMenuItem;
+	
+	// the model that this view is showing
+	private Model model;
+	
+	public MainMenuView(Model model_) {
+		
+		// set the model
+		model = model_;
+		model.addObserver(this);
+		
+		// create a menu UI with undo/redo
+		JMenu fileMenu = new JMenu("File");
+		JMenu editMenu = new JMenu("Edit");
+		this.add(fileMenu);
+		this.add(editMenu);
+		
+		// Create a "quit" menu item and add it to the file menu
+		JMenuItem quitMenuItem = new JMenuItem("Quit");
+		fileMenu.add(quitMenuItem);
+
+		// quit menu controller
+		quitMenuItem.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				System.exit(0);
+			}
+		});
+
+		// create undo and redo menu items
+		undoMenuItem = new JMenuItem("Undo");
+		undoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z,
+				ActionEvent.CTRL_MASK));
+		redoMenuItem = new JMenuItem("Redo");
+		redoMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y,
+				ActionEvent.CTRL_MASK));
+		editMenu.add(undoMenuItem);
+		editMenu.add(redoMenuItem);
+		
+		// controllers for undo menu item
+		undoMenuItem.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				model.undo();
+			}
+		});
+		// controller for redo menu item
+		redoMenuItem.addActionListener(new ActionListener() {
+			public void actionPerformed(ActionEvent event) {
+				model.redo();
+			}
+		});
+	}
+
+	@Override
+	public void update(Observable arg0, Object arg1) {
+		undoMenuItem.setEnabled(model.canUndo());
+		redoMenuItem.setEnabled(model.canRedo());	
+	}
+
+}
diff --git a/java/3-4-Undo/Model.java b/java/3-4-Undo/Model.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f3d60d6e9bbc692cb7f5dfad37ac93d9512e676
--- /dev/null
+++ b/java/3-4-Undo/Model.java
@@ -0,0 +1,170 @@
+// CS 349 Undo Demo
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.lang.reflect.Field;
+import java.util.Observable;
+
+import javax.swing.undo.*;
+
+// A simple model that is undoable
+public class Model extends Observable {
+
+	// Undo manager
+	private UndoManager undoManager;
+
+	// Model data
+	private int value;
+	private int min;
+	private int max;
+
+	Model(int value, int min, int max) {
+		undoManager = new UndoManager();
+		this.value = value;
+		this.min = min;
+		this.max = max;
+	}
+
+	public void updateViews() {
+		setChanged();
+		notifyObservers();
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public void setValue(int v) {
+		System.out.println("Model: set value to " + v);
+
+		// create undoable edit
+		UndoableEdit undoableEdit = new AbstractUndoableEdit() {
+
+			// capture variables for closure
+			final int oldValue = value;
+			final int newValue = v;
+
+			// Method that is called when we must redo the undone action
+			public void redo() throws CannotRedoException {
+				super.redo();
+				value = newValue;
+				System.out.println("Model: redo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+
+			public void undo() throws CannotUndoException {
+				super.undo();
+				value = oldValue;
+				System.out.println("Model: undo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+		};
+
+		// Add this undoable edit to the undo manager
+		undoManager.addEdit(undoableEdit);
+
+		// finally, set the value and notify views
+		value = v;
+		setChanged();
+		notifyObservers();
+	}
+
+	public void incrementValue() {
+		System.out.println("Model: increment value ");
+
+		// constrain value to valid range
+		if (value + 1 > max) return;
+
+		// create undoable edit
+		UndoableEdit undoableEdit = new AbstractUndoableEdit() {
+
+			// Method that is called when we must redo the undone action
+			public void redo() throws CannotRedoException {
+				super.redo();
+				value = value + 1;
+				System.out.println("Model: redo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+
+			public void undo() throws CannotUndoException {
+				super.undo();
+				value = value - 1;
+				System.out.println("Model: undo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+		};
+
+		// Add this undoable edit to the undo manager
+		undoManager.addEdit(undoableEdit);
+
+		value = value + 1;
+		setChanged();
+		notifyObservers();
+	}
+
+	public void decrementValue() {
+		System.out.println("Model: decrement value ");
+
+		// constrain value to valid range
+		if (value - 1 < min) return;
+
+		// create undoable edit
+		UndoableEdit undoableEdit = new AbstractUndoableEdit() {
+
+			// Method that is called when we must redo the undone action
+			public void redo() throws CannotRedoException {
+				super.redo();
+				value = value - 1;
+				System.out.println("Model: redo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+
+			public void undo() throws CannotUndoException {
+				super.undo();
+				value = value + 1;
+				System.out.println("Model: undo value to " + value);
+				setChanged();
+				notifyObservers();
+			}
+		};
+
+		// Add this undoable edit to the undo manager
+		undoManager.addEdit(undoableEdit);
+
+		value = value - 1;
+		setChanged();
+		notifyObservers();
+	}
+
+	// could make these settable and undoable too
+	public int getMin() { return min; }
+	public int getMax() { return max; }
+
+
+	// undo and redo methods
+	// - - - - - - - - - - - - - -
+
+	public void undo() {
+		if (canUndo())
+			undoManager.undo();
+	}
+
+	public void redo() {
+		if (canRedo())
+			undoManager.redo();
+	}
+
+	public boolean canUndo() {
+		return undoManager.canUndo();
+	}
+
+	public boolean canRedo() {
+		return undoManager.canRedo();
+	}
+
+}
diff --git a/java/3-4-Undo/View.java b/java/3-4-Undo/View.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7b38262c21f7f1ad20982b55696c03c49177dc2
--- /dev/null
+++ b/java/3-4-Undo/View.java
@@ -0,0 +1,114 @@
+// CS 349 Undo Demo
+
+import javax.swing.*;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.undo.AbstractUndoableEdit;
+import javax.swing.undo.UndoableEdit;
+
+import java.awt.*;
+import java.awt.event.*;
+import java.util.Observable;
+import java.util.Observer;
+
+class View extends JPanel implements Observer {
+
+	// the view's main user interface
+	private JSlider slider;
+	private JTextField text;
+	private JButton increment;
+	private JButton decrement;
+
+	// the model that this view is showing
+	private Model model;
+
+	View(Model model_) {
+
+		// set the model
+		model = model_;
+		model.addObserver(this);
+
+		// create the view UI
+		this.setLayout(new FlowLayout());
+
+		// slider to choose exact value
+		slider = new JSlider();
+		this.add(slider);
+				
+		// text boc to change value
+		text = new JTextField("X", 4);
+		this.add(text);
+
+		// increment/decrement buttons
+		decrement = new JButton("-");
+		increment = new JButton("+");
+		this.add(decrement);
+		this.add(increment);
+
+		this.add(new JLabel("shortcut keys: CTRL-Z to undo, CTRL-Y to redo"));
+
+		// change the flag and see what happens
+		final boolean noChunking = false;
+
+		if (noChunking) {
+			slider.addChangeListener(new ChangeListener() {
+
+				@Override
+				public void stateChanged(ChangeEvent arg0) {
+					model.setValue(slider.getValue());
+				}
+			});
+		} else {
+
+			// controller for when a drag event finishes
+			// this is the right undo "chunk"
+			slider.addMouseListener(new MouseAdapter() {
+				@Override
+				public void mouseReleased(MouseEvent e) {
+					model.setValue(slider.getValue());
+				}
+			});
+		}
+		
+		// add a controller for text edits too
+		// (will only fire when enter is pressed after editing ...)
+		text.addActionListener(new ActionListener() {
+			
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				try {
+					model.setValue(Integer.parseInt(text.getText()));				
+				} catch (NumberFormatException ex) {
+					// not a number, just update views to reset it
+					// (need to be careful here not to insert an undo)
+					model.updateViews();
+				}
+			}
+		});
+
+		increment.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				model.incrementValue();
+			}
+		});
+
+		decrement.addActionListener(new ActionListener() {
+			@Override
+			public void actionPerformed(ActionEvent e) {
+				model.decrementValue();
+			}
+		});
+	}
+
+	
+	// Observer interface
+	@Override
+	public void update(Observable arg0, Object arg1) {
+		System.out.println("View: update");
+		slider.setValue(model.getValue());
+		slider.setMinimum(model.getMin());
+		slider.setMaximum(model.getMax());
+		text.setText(Integer.toString(model.getValue()));
+	}
+}
diff --git a/java/3-4-Undo/makefile b/java/3-4-Undo/makefile
new file mode 100644
index 0000000000000000000000000000000000000000..2f843f1fb83fb74868ca6b0aa023df50fa80faad
--- /dev/null
+++ b/java/3-4-Undo/makefile
@@ -0,0 +1,15 @@
+# super simple makefile
+# call it using 'make NAME=name_of_code_file_without_extension'
+# (assumes a .java extension)
+NAME = "Main"
+
+all:
+	@echo "Compiling..."
+	javac *.java
+
+run: all
+	@echo "Running..."
+	java $(NAME)
+
+clean:
+	rm -rf *.class